import contextlib
import typing
import warnings
import pytest
from testsuite import annotations
from testsuite.utils import colors
from . import classes
from . import exceptions
from . import server
MOCKSERVER_DEFAULT_PORT = 9999
MOCKSERVER_SSL_DEFAULT_PORT = 9998
_SSL_KEY_FILE_INI_KEY = 'mockserver-ssl-key-file'
_SSL_CERT_FILE_INI_KEY = 'mockserver-ssl-cert-file'
MOCKSERVER_PORT_HELP = """
{proto} mockserver port for default worker.
Random port is used by default. If testsuite is started with
--service-wait or --service-disabled default is forced to {default}.
NOTE: non-default workers always use random port.
"""
class MockserverPlugin:
def __init__(self):
self._invalidators = set()
def pytest_runtest_call(self, item):
for invalidator in self._invalidators:
invalidator()
@contextlib.contextmanager
def register_invalidator(self, invalidator):
self._invalidators.add(invalidator)
try:
yield
finally:
self._invalidators.discard(invalidator)
def pytest_addoption(parser):
group = parser.getgroup('mockserver')
group.addoption(
'--mockserver-nofail',
action='store_true',
help='Do not fail if no handler is set.',
)
group.addoption(
'--mockserver-host',
default='localhost',
help='Default host for http mockserver.',
)
group.addoption(
'--mockserver-port',
type=int,
default=0,
help=MOCKSERVER_PORT_HELP.format(
proto='HTTP',
default=MOCKSERVER_DEFAULT_PORT,
),
)
group.addoption(
'--mockserver-ssl-host',
default='localhost',
help='Default host for https mockserver.',
)
group.addoption(
'--mockserver-ssl-port',
type=int,
default=0,
help=MOCKSERVER_PORT_HELP.format(
proto='HTTPS',
default=MOCKSERVER_SSL_DEFAULT_PORT,
),
)
group.addoption(
'--mockserver-unix-socket',
type=str,
help='Bind server to unix socket instead of tcp',
)
group.addoption(
'--mockserver-debug',
action='store_true',
help='Enable debugging logs.',
)
parser.addini(
'mockserver-tracing-enabled',
type='bool',
default=False,
help=(
'When request trace-id header not from testsuite:\n'
' True: handle, if handler missing return http status 500\n'
' False: handle, if handler missing raise '
'HandlerNotFoundError\n'
'When request trace-id header from other test:\n'
' True: do not handle, return http status 500\n'
' False: handle, if handler missing raise HandlerNotFoundError'
),
)
parser.addini(
'mockserver-trace-id-header',
default=server.DEFAULT_TRACE_ID_HEADER,
help=(
'name of tracing http header, value changes from test to test and '
'is constant within test'
),
)
parser.addini(
'mockserver-span-id-header',
default=server.DEFAULT_SPAN_ID_HEADER,
help='name of tracing http header, value is unique for each request',
)
parser.addini(
'mockserver-ssl-cert-file',
type='pathlist',
help='path to ssl certificate file to setup mockserver_ssl',
)
parser.addini(
'mockserver-ssl-key-file',
type='pathlist',
help='path to ssl key file to setup mockserver_ssl',
)
parser.addini(
'mockserver-http-proxy-enabled',
type='bool',
default=False,
help='If enabled mockserver acts as http proxy',
)
def pytest_configure(config):
config.pluginmanager.register(MockserverPlugin(), 'mockserver_plugin')
config.addinivalue_line(
'markers',
'mockserver_nosetup_errors: do not fail on mockserver setup errors',
)
def pytest_register_object_hooks():
return {
'$mockserver': {'$fixture': '_mockserver_hook'},
'$mockserver_https': {'$fixture': '_mockserver_https_hook'},
}
@pytest.fixture(name='_mockserver_create_session')
def fixture_mockserver_create_session(
_mockserver_trace_id: str,
_mockserver_errors_clear,
):
@contextlib.contextmanager
def create_session(mockserver):
__tracebackhide__ = True
with mockserver.new_session(_mockserver_trace_id) as session:
with _mockserver_errors_clear(session):
yield server.MockserverFixture(mockserver, session)
return create_session
[docs]@pytest.fixture
def mockserver(
_mockserver: server.Server,
_mockserver_create_session,
) -> annotations.YieldFixture[server.MockserverFixture]:
with _mockserver_create_session(_mockserver) as fixture:
yield fixture
@pytest.fixture
async def mockserver_ssl(
_mockserver_ssl: typing.Optional[server.Server],
_mockserver_create_session,
) -> annotations.AsyncYieldFixture[server.MockserverSslFixture]:
if _mockserver_ssl is None:
raise exceptions.MockServerError(
f'mockserver_ssl is not configured. {_SSL_KEY_FILE_INI_KEY} and '
f'{_SSL_CERT_FILE_INI_KEY} must be specified in pytest.ini',
)
with _mockserver_create_session(_mockserver_ssl) as fixture:
yield fixture
[docs]@pytest.fixture(scope='session')
def mockserver_info(_mockserver: server.Server) -> classes.MockserverInfo:
"""Returns mockserver information object."""
return _mockserver.server_info
@pytest.fixture(scope='session')
def mockserver_ssl_info(
_mockserver_ssl: typing.Optional[server.Server],
) -> typing.Optional[classes.MockserverInfo]:
if _mockserver_ssl is None:
return None
return _mockserver_ssl.server_info
@pytest.fixture(scope='session')
def mockserver_ssl_cert(pytestconfig) -> typing.Optional[classes.SslCertInfo]:
def _get_ini_path(name):
values = pytestconfig.getini(name)
if not values:
return None
if len(values) > 1:
raise exceptions.MockServerError(
f'{name} ini setting has multiple values',
)
return str(values[0])
cert_path = _get_ini_path(_SSL_CERT_FILE_INI_KEY)
key_path = _get_ini_path(_SSL_KEY_FILE_INI_KEY)
if cert_path and key_path:
return classes.SslCertInfo(
cert_path=cert_path,
private_key_path=key_path,
)
return None
@pytest.fixture(scope='session')
def _mockserver_getport(pytestconfig, worker_id):
def getport(option_port, default_port):
# Cannot use same port under xdist
if worker_id != 'master':
return 0
# If service is started outside of testsuite use constant
# port by default.
if (
pytestconfig.option.service_wait
or pytestconfig.option.service_disable
):
if option_port == 0:
return default_port
return option_port
return getport
@pytest.fixture(scope='session')
async def _mockserver(
pytestconfig,
loop,
_mockserver_getport,
) -> annotations.AsyncYieldFixture[server.Server]:
if pytestconfig.option.mockserver_unix_socket:
async with server.create_unix_server(
socket_path=pytestconfig.option.mockserver_unix_socket,
loop=loop,
pytestconfig=pytestconfig,
) as result:
yield result
else:
port = _mockserver_getport(
pytestconfig.option.mockserver_port,
MOCKSERVER_DEFAULT_PORT,
)
async with server.create_server(
host=pytestconfig.option.mockserver_host,
port=port,
loop=loop,
pytestconfig=pytestconfig,
ssl_info=None,
) as result:
yield result
@pytest.fixture(scope='session')
async def _mockserver_ssl(
pytestconfig,
loop,
mockserver_ssl_cert,
_mockserver_getport,
) -> annotations.AsyncYieldFixture[typing.Optional[server.Server]]:
if mockserver_ssl_cert:
port = _mockserver_getport(
pytestconfig.option.mockserver_ssl_port,
MOCKSERVER_SSL_DEFAULT_PORT,
)
async with server.create_server(
host=pytestconfig.option.mockserver_ssl_host,
port=port,
loop=loop,
pytestconfig=pytestconfig,
ssl_info=mockserver_ssl_cert,
) as result:
yield result
else:
yield None
@pytest.fixture
def _mockserver_trace_id() -> str:
return server.generate_trace_id()
@pytest.fixture(scope='session')
def _mockserver_hook(mockserver_info):
def wrapper(doc: dict):
return _mockserver_info_hook(doc, '$mockserver', mockserver_info)
return wrapper
@pytest.fixture(scope='session')
def _mockserver_https_hook(mockserver_ssl_info):
def wrapper(doc: dict):
return _mockserver_info_hook(
doc,
'$mockserver_https',
mockserver_ssl_info,
)
return wrapper
@pytest.fixture(name='_mockserver_errors_clear')
def fixture_mockserver_errors_clear(
_mockserver_plugin: MockserverPlugin,
request,
mockserver_nosetup_errors,
):
"""
Clear mockserver errors at startup.
Required for backward compatibility with older testsuite versions.
"""
marker = request.node.get_closest_marker('mockserver_nosetup_errors')
if marker:
warnings.warn(
'pytest.mark.mockserver_nosetup_errors is for backward '
'compatibility only, please rewrite your code',
DeprecationWarning,
)
mockserver_nosetup_errors = True
@contextlib.contextmanager
def errors_clear(session: server.Session):
if not mockserver_nosetup_errors:
yield
return
with _mockserver_plugin.register_invalidator(session.clear_errors):
yield
return errors_clear
@pytest.fixture(name='mockserver_nosetup_errors', scope='session')
def fixture_mockserver_nosetup_errors():
return False
@pytest.fixture(name='_mockserver_plugin', scope='session')
def fixture_mockserver_plugin(pytestconfig) -> MockserverPlugin:
return pytestconfig.pluginmanager.get_plugin('mockserver_plugin')
def _mockserver_info_hook(doc: dict, key=None, mockserver_info=None):
if mockserver_info is None:
raise RuntimeError(f'Missing {key} argument')
if not doc.get('$schema', True):
schema = ''
elif mockserver_info.ssl is not None:
schema = 'https://'
else:
schema = 'http://'
return '%s%s:%d%s' % (
schema,
mockserver_info.host,
mockserver_info.port,
doc[key],
)