userver: en/testsuite/mockserver/pytest_plugin.py Source File
Loading...
Searching...
No Matches
pytest_plugin.py
1import asyncio
2import contextlib
3import logging
4import warnings
5
6import pytest
7
8from testsuite import types
9from testsuite.tracing import TraceidManager
10from testsuite.utils import net
11
12from . import classes, exceptions, server
13
14MOCKSERVER_DEFAULT_PORT = 9999
15MOCKSERVER_SSL_DEFAULT_PORT = 9998
16
17_SSL_KEY_FILE_INI_KEY = 'mockserver-ssl-key-file'
18_SSL_CERT_FILE_INI_KEY = 'mockserver-ssl-cert-file'
19
20MOCKSERVER_PORT_HELP = """
21{proto} mockserver port for default worker.
22Random port is used by default. If testsuite is started with
23--service-wait or --service-disabled default is forced to {default}.
24"""
25
26logger = logging.getLogger(__name__)
27
28
29def pytest_addoption(parser):
30 group = parser.getgroup('mockserver')
31 group.addoption(
32 '--mockserver-nofail',
33 action='store_true',
34 help='Do not fail if no handler is set.',
35 )
36 group.addoption(
37 '--mockserver-host',
38 default='localhost',
39 help='Default host for http mockserver.',
40 )
41 group.addoption(
42 '--mockserver-port',
43 type=int,
44 default=0,
45 help=MOCKSERVER_PORT_HELP.format(
46 proto='HTTP',
47 default=MOCKSERVER_DEFAULT_PORT,
48 ),
49 )
50 group.addoption(
51 '--mockserver-ssl-host',
52 default='localhost',
53 help='Default host for https mockserver.',
54 )
55 group.addoption(
56 '--mockserver-ssl-port',
57 type=int,
58 default=0,
59 help=MOCKSERVER_PORT_HELP.format(
60 proto='HTTPS',
61 default=MOCKSERVER_SSL_DEFAULT_PORT,
62 ),
63 )
64 group.addoption(
65 '--mockserver-unix-socket',
66 type=str,
67 help='Bind server to unix socket instead of tcp',
68 )
69 group.addoption(
70 '--mockserver-debug',
71 action='store_true',
72 help='Enable debugging logs.',
73 )
74 parser.addini(
75 'mockserver-tracing-enabled',
76 type='bool',
77 default=False,
78 help=(
79 'When request trace-id header not from testsuite:\n'
80 ' True: handle, if handler missing return http status 500\n'
81 ' False: handle, if handler missing raise '
82 'HandlerNotFoundError\n'
83 'When request trace-id header from other test:\n'
84 ' True: do not handle, return http status 500\n'
85 ' False: handle, if handler missing raise HandlerNotFoundError'
86 ),
87 )
88 parser.addini(
89 'mockserver-trace-id-header',
90 default=server.DEFAULT_TRACE_ID_HEADER,
91 help=(
92 'name of tracing http header, value changes from test to test and '
93 'is constant within test'
94 ),
95 )
96 parser.addini(
97 'mockserver-span-id-header',
98 default=server.DEFAULT_SPAN_ID_HEADER,
99 help='name of tracing http header, value is unique for each request',
100 )
101 parser.addini(
102 'mockserver-ssl-cert-file',
103 type='pathlist',
104 help='path to ssl certificate file to setup mockserver_ssl',
105 )
106 parser.addini(
107 'mockserver-ssl-key-file',
108 type='pathlist',
109 help='path to ssl key file to setup mockserver_ssl',
110 )
111 parser.addini(
112 'mockserver-http-proxy-enabled',
113 type='bool',
114 default=False,
115 help='If enabled mockserver acts as http proxy',
116 )
117
118
119def pytest_configure(config):
120 config.addinivalue_line(
121 'markers',
122 'mockserver_assert_lost_calls: assert that all calls to mockservers are checked',
123 )
124
125
126def pytest_register_object_hooks():
127 return {
128 '$mockserver': {'$fixture': '_mockserver_hook'},
129 '$mockserver_https': {'$fixture': '_mockserver_https_hook'},
130 }
131
132
133@pytest.fixture(name='mockserver_strict_default')
134def fixture_mockserver_strict_default():
135 return False
136
137
138@pytest.fixture(name='mockserver_create_session')
139def fixture_mockserver_create_session(
140 request,
141 asyncexc_append,
142 testsuite_traceid_manager: TraceidManager,
143 mockserver_strict_default: bool,
144):
145 assert_lost_calls = request.node.get_closest_marker(
146 'mockserver_assert_lost_calls'
147 )
148
149 @contextlib.contextmanager
150 def create_session(mockserver):
151 with mockserver.new_session(
152 asyncexc_append=asyncexc_append,
153 traceid_manager=testsuite_traceid_manager,
154 ) as session:
155 yield server.MockserverFixture(
156 mockserver,
157 session,
158 strict_default=mockserver_strict_default,
159 )
160
161 calls = session.collect_calls()
162 if assert_lost_calls:
163 if not calls:
164 raise exceptions.MockServerError(
165 f'mockserver is expected to have lost calls, but it doesnt'
166 )
167 else:
168 if calls:
169 raise exceptions.MockServerError(
170 f'mockserver handler with strict=True has skipped calls: {calls}'
171 )
172
173 return create_session
174
175
176@pytest.fixture(name='_mockserver_create_session')
177def legacy_fixture_mockserver_create_session(
178 mockserver_create_session,
179):
180 def create_session(*args, **kwargs):
181 warnings.warn(
182 'Use mockserver_create_session() fixture instead',
183 DeprecationWarning,
184 )
185 return mockserver_create_session(*args, **kwargs)
186
187 return create_session
188
189
190@pytest.fixture
191def mockserver(
192 _mockserver: server.Server,
193 mockserver_create_session,
194) -> types.YieldFixture[server.MockserverFixture]:
195 """Returns per-test mockserver client.
196
197 Use it to register HTTP handlers that the service under test can call.
198
199 @ingroup userver_testsuite_fixtures
200 Part of the [yandex-taxi-testsuite](https://github.com/yandex/yandex-taxi-testsuite/blob/develop/testsuite/mockserver/pytest_plugin.py#L191)
201 """
202 with mockserver_create_session(_mockserver) as fixture:
203 yield fixture
204
205
206@pytest.fixture
207def mockserver_ssl(
208 _mockserver_ssl: server.Server | None,
209 mockserver_create_session,
210) -> types.AsyncYieldFixture[server.MockserverSslFixture]:
211 if _mockserver_ssl is None:
212 raise exceptions.MockServerError(
213 f'mockserver_ssl is not configured. {_SSL_KEY_FILE_INI_KEY} and '
214 f'{_SSL_CERT_FILE_INI_KEY} must be specified in pytest.ini',
215 )
216 with mockserver_create_session(_mockserver_ssl) as fixture:
217 yield fixture
218
219
220@pytest.fixture(scope='session')
221def mockserver_info(
222 _mockserver_socket: classes.MockserverSocket,
223) -> classes.MockserverInfo:
224 """Returns mockserver information object.
225
226 @ingroup userver_testsuite_fixtures
227 Part of the [yandex-taxi-testsuite](https://github.com/yandex/yandex-taxi-testsuite/blob/develop/testsuite/mockserver/pytest_plugin.py#L218)
228 """
229 return _mockserver_socket.info
230
231
232@pytest.fixture(scope='session')
233def mockserver_ssl_info(
234 _mockserver_ssl_socket: classes.MockserverSocket | None,
235) -> classes.MockserverInfo | None:
236 if _mockserver_ssl_socket is None:
237 return None
238 return _mockserver_ssl_socket.info
239
240
241@pytest.fixture(scope='session')
242def mockserver_ssl_cert(pytestconfig) -> classes.SslCertInfo | None:
243 def _get_ini_path(name):
244 values = pytestconfig.getini(name)
245 if not values:
246 return None
247 if len(values) > 1:
248 raise exceptions.MockServerError(
249 f'{name} ini setting has multiple values',
250 )
251 return str(values[0])
252
253 cert_path = _get_ini_path(_SSL_CERT_FILE_INI_KEY)
254 key_path = _get_ini_path(_SSL_KEY_FILE_INI_KEY)
255 if cert_path and key_path:
256 return classes.SslCertInfo(
257 cert_path=cert_path,
258 private_key_path=key_path,
259 )
260 return None
261
262
263@pytest.fixture(scope='session')
264async def mockserver_create(
265 _mockserver_config,
266):
267 @contextlib.asynccontextmanager
268 async def create(
269 *,
270 host='localhost',
271 port=0,
272 socket_path=None,
273 ssl_cert: classes.SslCertInfo | None = None,
274 config: classes.MockserverConfig | None = None,
275 ):
276 socket_info = server._create_mockserver_socket(
277 host=host,
278 port=port,
279 socket_path=socket_path,
280 https=bool(ssl_cert),
281 )
282 async with server._create_server_from_socket(
283 socket_info,
284 config or _mockserver_config,
285 ssl_cert=ssl_cert,
286 ) as result:
287 yield result
288
289 return create
290
291
292@pytest.fixture(scope='session')
293async def _mockserver(
294 pytestconfig,
295 _mockserver_socket: classes.MockserverSocket,
296 _mockserver_config: classes.MockserverConfig,
297) -> types.AsyncYieldFixture[server.Server]:
298 async with server._create_server_from_socket(
299 _mockserver_socket, _mockserver_config
300 ) as result:
301 yield result
302
303
304@pytest.fixture(scope='session')
305async def _mockserver_ssl(
306 pytestconfig,
307 _mockserver_ssl_socket: classes.MockserverSocket,
308 _mockserver_config: classes.MockserverConfig,
309 mockserver_ssl_cert,
310) -> types.AsyncYieldFixture[server.Server]:
311 if mockserver_ssl_cert:
312 async with server._create_server_from_socket(
313 _mockserver_ssl_socket,
314 _mockserver_config,
315 ssl_cert=mockserver_ssl_cert,
316 ) as result:
317 yield result
318 else:
319 yield None
320
321
322@pytest.fixture(scope='session')
323def _mockserver_hook(mockserver_info):
324 def wrapper(doc: dict):
325 return _mockserver_info_hook(doc, '$mockserver', mockserver_info)
326
327 return wrapper
328
329
330@pytest.fixture(scope='session')
331def _mockserver_https_hook(mockserver_ssl_info):
332 def wrapper(doc: dict):
333 return _mockserver_info_hook(
334 doc,
335 '$mockserver_https',
336 mockserver_ssl_info,
337 )
338
339 return wrapper
340
341
342@pytest.fixture(scope='session')
343def _mockserver_socket(
344 pytestconfig, _mockserver_config
345) -> classes.MockserverSocket:
346 port = _mockserver_getport(
347 pytestconfig,
348 pytestconfig.option.mockserver_port,
349 default_port=MOCKSERVER_DEFAULT_PORT,
350 )
351 mockserver_socket = server._create_mockserver_socket(
352 socket_path=pytestconfig.option.mockserver_unix_socket,
353 host=pytestconfig.option.mockserver_host,
354 port=port,
355 )
356 with net.closing_sockets(mockserver_socket.sockets):
357 info = []
358 for sock in mockserver_socket.sockets:
359 info.append(sock.getsockname())
360 logger.debug('Mockserver bound to %r', info)
361 yield mockserver_socket
362
363
364@pytest.fixture(scope='session')
365def _mockserver_ssl_socket(
366 pytestconfig,
367) -> classes.MockserverSocket | None:
368 port = _mockserver_getport(
369 pytestconfig,
370 pytestconfig.option.mockserver_ssl_port,
371 default_port=MOCKSERVER_SSL_DEFAULT_PORT,
372 )
373 mockserver_socket = server._create_mockserver_socket(
374 host=pytestconfig.option.mockserver_ssl_host,
375 port=port,
376 https=True,
377 )
378 with net.closing_sockets(mockserver_socket.sockets):
379 info = []
380 for sock in mockserver_socket.sockets:
381 info.append(sock.getsockname())
382 logger.debug('Mockserver HTTPS bound to %r', info)
383 yield mockserver_socket
384
385
386@pytest.fixture(scope='session')
387async def mockserver_set_debug(_mockserver, _mockserver_ssl):
388 def set_debug(enabled: bool):
389 loop = asyncio.get_running_loop()
390 for obj in (loop, _mockserver, _mockserver_ssl):
391 if obj is not None:
392 obj.set_debug(enabled)
393
394 return set_debug
395
396
397@pytest.fixture(scope='session')
398def _mockserver_config(pytestconfig) -> classes.MockserverConfig:
399 return classes.MockserverConfig(
400 nofail=pytestconfig.option.mockserver_nofail,
401 debug=pytestconfig.option.mockserver_debug,
402 tracing_enabled=pytestconfig.getini('mockserver-tracing-enabled'),
403 trace_id_header=pytestconfig.getini('mockserver-trace-id-header'),
404 span_id_header=pytestconfig.getini('mockserver-span-id-header'),
405 http_proxy_enabled=pytestconfig.getini('mockserver-http-proxy-enabled'),
406 )
407
408
409def _mockserver_info_hook(
410 doc: dict, key=None, mockserver_info: classes.MockserverInfo | None = None
411):
412 if mockserver_info is None:
413 raise RuntimeError(f'Missing {key} argument')
414 if not doc.get('$schema', True):
415 schema = ''
416 elif mockserver_info.https:
417 schema = 'https://'
418 else:
419 schema = 'http://'
420 return '%s%s:%d%s' % (
421 schema,
422 mockserver_info.host,
423 mockserver_info.port,
424 doc[key],
425 )
426
427
428def _mockserver_getport(config, option_port, default_port):
429 # If service is started outside of testsuite use constant
430 # port by default.
431 if config.option.service_wait or config.option.service_disable:
432 if option_port == 0:
433 return default_port
434 return option_port