16from testsuite
import utils
26from .
import classes, exceptions, magicargs
27from .exceptions
import __tracebackhide__
29DEFAULT_TRACE_ID_HEADER =
'X-YaTraceId'
30DEFAULT_SPAN_ID_HEADER =
'X-YaSpanId'
32REQUEST_FROM_ANOTHER_TEST_ERROR =
'Internal error: request is from other test'
34_SUPPORTED_ERRORS_HEADER =
'X-Testsuite-Supported-Errors'
35_ERROR_HEADER =
'X-Testsuite-Error'
38 (
'X-YaTraceId',
'trace_id'),
39 (
'X-YaSpanId',
'span_id'),
40 (
'X-YaRequestId',
'link'),
43logger = logging.getLogger(__name__)
45RouteParams = dict[str, str]
50 def __init__(self, message, *args, **kwargs):
51 super().__init__(message, *args, **kwargs)
58 func: typing.Callable,
60 raw_request: bool =
False,
61 json_response: bool =
False,
71 return callinfo.acallqueue(self.
orig_func)
74 def handler_args(self):
82 f
'{Handler.__module__}.{Handler.__name__}({self.orig_func!r}, '
83 f
'raw_request={self.handler_args.raw_request}, '
84 f
'json_response={self.json_response})'
87 async def __call__(self, request: aiohttp.web.BaseRequest, **kwargs):
88 args, kwargs = await self.
handler_args.build_args(request, kwargs)
89 response = await self.
callqueue(*args, **kwargs)
92 if isinstance(response, (
http.Response, aiohttp.web.Response)):
94 return http.make_response(json=response)
96 def collect_calls(self) -> list[dict]:
102 while callqueue.has_calls:
103 lost_calls.append(callqueue.next_call())
108 handlers: dict[str, Handler]
109 prefix_handlers: list[tuple[str, Handler]]
110 regex_handlers: list[tuple[typing.Pattern, Handler]]
116 traceid_manager: TraceidManager,
117 tracing_enabled=
True,
118 http_proxy_enabled=
False,
119 mockserver_host=
None,
130 def get_handler(self, path: str) -> tuple[Handler, RouteParams]:
132 if handler
is not None:
135 match = pattern.fullmatch(path)
137 return handler, match.groupdict()
139 if path.startswith(prefix):
145 def _get_handler_not_found_message(self, path: str) -> str:
147 tracing_state =
'enabled'
149 tracing_state =
'disabled'
152 handlers_list =
'\n'.join(
154 (f
'- {path}' for path
in self.
handlers),
155 (f
'- REGEX {pattern}' for pattern
in patterns),
156 (f
'- PREFIX {prefix}' for prefix
in prefixes),
160 f
'Mockserver handler is not installed for {path!r}.\n\n'
161 f
'Perhaps you forgot to setup mockserver handler. '
162 f
'Tracing: {tracing_state}. Installed handlers:\n'
166 async def handle_request(
168 request: MockserverRequest,
176 return _internal_error(f
'Internal server error: {exc!r}')
179 response = await handler(request, **kwargs)
181 return response.to_aiohttp()
183 response, (aiohttp.web.Response, aiohttp.web.WebSocketResponse)
187 return _mocked_error_response(request, response.error_code)
189 'http.Response or aiohttp.web.Response instance is expected '
190 f
'{response!r} given',
193 return _mocked_error_response(request, exc.error_code)
194 except Exception
as exc:
196 return _internal_error(f
'Internal server error: {exc!r}')
198 def register_handler(
203 prefix: bool =
False,
209 'Parameter value prefix=True is not supported if regex '
210 'parameter is also True.',
212 pattern = re.compile(path)
221 def _get_handler_for_request(
223 request: MockserverRequest,
224 ) -> tuple[Handler, RouteParams]:
225 path = request.original_path
227 host = request.headers.get(
'host')
232 def collect_calls(self) -> list[dict]:
235 calls.extend(handler.collect_calls())
237 calls.extend(handler.collect_calls())
238 for handler
in self.
handlers.values():
239 calls.extend(handler.collect_calls())
249 mockserver_info: classes.MockserverInfo,
252 mockserver_debug=
False,
253 tracing_enabled=
True,
254 trace_id_header=DEFAULT_TRACE_ID_HEADER,
255 span_id_header=DEFAULT_SPAN_ID_HEADER,
256 http_proxy_enabled=
False,
258 self.
_info = mockserver_info
267 def tracing_enabled(self) -> bool:
270 return self.
session.tracing_enabled
273 def trace_id_header(self):
277 def span_id_header(self):
281 def http_proxy_enabled(self):
285 def server_info(self) -> classes.MockserverInfo:
288 def get_debug(self) -> bool:
291 def set_debug(self, enabled: bool):
294 @contextlib.contextmanager
299 traceid_manager: TraceidManager,
302 asyncexc_append=asyncexc_append,
304 traceid_manager=traceid_manager,
306 mockserver_host=self.
_info.get_host_header(),
313 async def handle_request(self, request):
314 started = time.perf_counter()
319 except BaseException
as exc:
323 def _log_request(self, started, request, response=None, exc=None):
327 '_type':
'mockserver_request',
328 'timestamp': utils.utcnow(),
329 'method': request.method,
330 'url': request.rel_url,
332 for header, key
in _LOGGER_HEADERS:
333 if header
in request.headers:
334 fields[key] = request.headers[header]
335 delay_ms = 1000 * (time.perf_counter() - started)
336 fields[
'delay'] = f
'{delay_ms:.3f}ms'
337 if response
is not None:
338 log_level = logging.DEBUG
339 fields[
'meta_code'] = response.status
340 fields[
'status'] =
'DONE'
342 log_level = logging.ERROR
343 fields[
'status'] =
'FAIL'
344 fields[
'exc_info'] = str(exc)
345 logger.log(log_level,
'Mockserver request', extra={
'tskv': fields})
347 async def _handle_request(self, request: MockserverRequest):
350 'Internal error: mockserver session was not initialized'
355 traceid_manager = self.
session.traceid_manager
356 if self.
tracing_enabled and not traceid_manager.is_testsuite(trace_id):
361 return _internal_error(REQUEST_FROM_ANOTHER_TEST_ERROR)
363 return await self.
session.handle_request(request, nofail_404=nofail)
365 return _internal_error(
366 'Internal error: mockserver handler not found',
369 def _report_other_test_request(self, request, trace_id):
371 'Mockserver called path %s with previous test trace_id %s',
378 """Mockserver handler installer fixture."""
384 base_prefix: str =
'',
386 strict_default: bool =
False,
394 def new(self, prefix: str) ->
'MockserverFixture':
395 """Create mockserver installer with given base prefix."""
403 def base_url(self) -> str:
404 """Mockserver base url."""
405 return self.
_server.server_info.base_url
409 """Mockserver hostname."""
410 return self.
_server.server_info.host
414 """Mockserver port."""
415 return self.
_server.server_info.port
418 def trace_id_header(self) -> str:
419 return self.
_server.trace_id_header
422 def span_id_header(self) -> str:
423 return self.
_server.span_id_header
426 def trace_id(self) -> str:
427 return self.
_session.traceid_manager.trace_id
433 prefix: bool =
False,
434 raw_request: bool =
False,
435 json_response: bool =
False,
437 strict: typing.Optional[bool] =
None,
438 ) -> classes.GenericRequestDecorator:
439 """Register basic http handler for ``path``.
441 Returns decorator that registers handler ``path``. Original function is
442 wrapped with :ref:`AsyncCallQueue`.
444 :param path: match url by prefix if ``True`` exact match otherwise
445 :param raw_request: pass ``aiohttp.web.Response`` to handler instead of
446 ``testsuite.utils.http.Request``
447 :param regex: set True to match path as regex pattern
448 :param prefix: set True to match path prefix instead of whole path
449 :param json_response: set True to let handler return json object
450 instead of full response object
452 .. code-block:: python
454 @mockserver.handler('/service/path')
455 def handler(request: testsuite.utils.http.Request):
456 return mockserver.make_response('Hello, world!')
461 'raw_request=True is deprecated, use aiohttp_handler() instead',
466 'json_response=True is deprecated, use json_handler() instead',
473 raw_request=raw_request,
474 json_response=json_response,
483 prefix: bool =
False,
484 raw_request: bool =
False,
486 strict: typing.Optional[bool] =
None,
487 ) -> classes.JsonRequestDecorator:
488 """Register json http handler for ``path``.
490 Returns decorator that registers handler ``path``. Original function is
491 wrapped with :ref:`AsyncCallQueue`.
493 :param path: match url by prefix if ``True`` exact match otherwise
494 :param raw_request: pass ``aiohttp.web.Response`` to handler instead of
495 ``testsuite.utils.http.Request``
496 :param prefix: set True to match path prefix instead of whole path
497 :param regex: set True to match path as regex pattern
499 .. code-block:: python
501 @mockserver.json_handler('/service/path')
502 def handler(request: testsuite.utils.http.Request):
503 # Return JSON document
505 # or call to mockserver.make_response()
506 return mockserver.make_response(...)
510 'raw_request=True is deprecated, '
511 'use aiohttp_json_handler() instead',
517 raw_request=raw_request,
527 prefix: bool =
False,
529 strict: typing.Optional[bool] =
None,
530 ) -> classes.GenericRequestDecorator:
540 def aiohttp_json_handler(
544 prefix: bool =
False,
546 strict: typing.Optional[bool] =
None,
547 ) -> classes.JsonRequestDecorator:
557 def url(self, path: str) -> str:
558 """Builds mockserver url for ``path``"""
559 return url_util.join(self.
base_url, path)
562 """Builds mockserver url for ``path``"""
563 return yarl.URL(url_util.join(self.
base_url, path), encoded=
True)
565 def ws_url(self, path: str) -> str:
566 return self.
_server.server_info.ws_url(path)
568 def ignore_trace_id(self) -> typing.ContextManager[None]:
571 @contextlib.contextmanager
572 def tracing(self, value: bool =
True):
573 original_value = self.
_session.tracing_enabled
575 self.
_session.tracing_enabled = value
578 self.
_session.tracing_enabled = original_value
580 def get_callqueue_for(self, path) -> callinfo.AsyncCallQueue:
581 handler, _ = self.
_session.get_handler(path)
582 return handler.callqueue
584 make_response = staticmethod(http.make_response)
589 def _handler_installer(
593 strict: typing.Optional[bool],
594 prefix: bool =
False,
595 raw_request: bool =
False,
596 json_response: bool =
False,
598 ) -> typing.Callable:
606 raw_request=raw_request,
607 json_response=json_response,
616 return handler.callqueue
620 def _build_fullpath(self, path, regex: bool =
False) -> str:
624 if self.
_server.http_proxy_enabled
and path.startswith(
'http://'):
630MockserverSslFixture = MockserverFixture
641 warnings.warn(
'Use mockserver_create() fixture instead', DeprecationWarning)
643 mockserver_socket = _create_mockserver_socket(host=host, port=port)
644 return _create_server_from_socket(
652def _create_ssl_context(ssl_cert: classes.SslCertInfo) -> ssl.SSLContext:
653 ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
654 ssl_context.load_cert_chain(ssl_cert.cert_path, ssl_cert.private_key_path)
658def _internal_error(message: str =
'Internal error') -> aiohttp.web.Response:
659 return http.make_response(message, status=500).to_aiohttp()
662def _mocked_error_response(request, error_code) -> aiohttp.web.Response:
663 if _SUPPORTED_ERRORS_HEADER
not in request.headers:
665 'Service does not support mockserver errors protocol',
667 supported_errors = request.headers[_SUPPORTED_ERRORS_HEADER].split(
',')
668 if error_code
not in supported_errors:
670 f
'Service does not support mockserver error of type {error_code}',
672 return http.make_response(
675 headers={_ERROR_HEADER: error_code},
679def _create_server_obj(
680 mockserver_info: classes.MockserverInfo,
685 nofail=mockserver_config.nofail,
686 mockserver_debug=mockserver_config.debug,
687 tracing_enabled=mockserver_config.tracing_enabled,
688 trace_id_header=mockserver_config.trace_id_header,
689 span_id_header=mockserver_config.span_id_header,
690 http_proxy_enabled=mockserver_config.http_proxy_enabled,
694def _create_web_server(server: Server, loop) -> aiohttp.web.Server:
695 def request_factory(*args):
698 return aiohttp.web.Server(
699 server.handle_request,
700 request_factory=request_factory,
706def _create_mockserver_socket(
712 if socket_path
is None:
713 sockets = net_utils.bind_socket_multiple(host, port)
715 sockets = [net_utils.bind_unix_socket(socket_path)]
718 sock.setblocking(
False)
719 info = _create_mockserver_info(
721 socket_path=socket_path,
728@contextlib.asynccontextmanager
729async def _create_server_from_socket(
730 mockserver_socket: classes.MockserverSocket,
734) -> typing.AsyncGenerator[Server,
None]:
736 ssl_context = _create_ssl_context(ssl_cert)
741 loop = asyncio.get_running_loop()
743 server = _create_server_obj(mockserver_socket.info, mockserver_config)
744 web_server = _create_web_server(server, loop)
746 async with net_utils.create_server_multiple(
748 sockets=mockserver_socket.sockets,
754def _create_mockserver_info(
761 return _create_unix_mockserver_info(socket_path)
762 port = sock.getsockname()[1]
763 schema =
'https' if https
else 'http'
764 base_url = f
'{schema}://{host}:{port}/'
773def _create_unix_mockserver_info(
774 socket_path: pathlib.Path,
777 socket_path=socket_path,
779 base_url=
'http://localhost/',
786def _path_from_message(message):
787 """Returns original HTTP path without query."""
788 path = str(message.url)
789 path = path.split(
'?')[0]
790 path = urllib.parse.unquote(path)