v1.3.5
This commit is contained in:
@@ -0,0 +1,499 @@
|
||||
########################################################################
|
||||
# File name: __init__.py
|
||||
# This file is part of: aioxmpp
|
||||
#
|
||||
# LICENSE
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program. If not, see
|
||||
# <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
########################################################################
|
||||
"""
|
||||
:mod:`~aioxmpp.e2etest` --- Framework for writing integration tests for :mod:`aioxmpp`
|
||||
######################################################################################
|
||||
|
||||
This subpackage provides utilities for writing end-to-end or intgeration tests
|
||||
for :mod:`aioxmpp` components.
|
||||
|
||||
.. warning::
|
||||
|
||||
For now, the API of this subpackage is classified as internal. Please do not
|
||||
test your external components using this API, as it is experimental and
|
||||
subject to change.
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
The basic concept is that tests are written like normal unittests. However,
|
||||
tests are written by inheriting classes from :class:`aioxmpp.e2etest.TestCase`
|
||||
instead of :mod:`unittest.TestCase`. :class:`.e2etest.TestCase` has the
|
||||
:attr:`~.e2etest.TestCase.provisioner` attribute which provides access to a
|
||||
:class:`.provision.Provisioner` instance.
|
||||
|
||||
Provisioners are objects which provide a way to obtain a connected XMPP client.
|
||||
The JID to which the client is bound is unspecified; however, each client gets
|
||||
a unique bare JID and the clients are able to communicate with each other. In
|
||||
addition, provisioners provide information about the environment in which the
|
||||
clients act. This includes providing JIDs of entities implementing specific
|
||||
protocols or features. The details are explained in the documentation of the
|
||||
:class:`~.provision.Provisioner` base class.
|
||||
|
||||
By default, tests which are written with :class:`.e2etest.TestCase` are skipped
|
||||
when using the normal test runners. This is because the provisioners need to be
|
||||
configured; this is handled using a custom nosetests plugin which is not loaded
|
||||
by default (for good reasons). To run the tests, use (instead of the normal
|
||||
``nosetests3`` binary):
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ python3 -m aioxmpp.e2etest
|
||||
|
||||
The command line interface is identical to the one of ``nosetests3``, except
|
||||
that additional options are provided to configure the plugin. In fact,
|
||||
:mod:`aioxmpp.e2etest` is simply a nose test runner with an additional plugin.
|
||||
|
||||
By default, the configuration is read from ``./.local/e2etest.ini``. For
|
||||
details on configuring the provisioners, see :ref:`the developer guide
|
||||
<dg-end-to-end-tests>`.
|
||||
|
||||
Main API
|
||||
========
|
||||
|
||||
Decorators for test methods
|
||||
---------------------------
|
||||
|
||||
The following decorators can be used on test methods (including ``setUp`` and
|
||||
``tearDown``):
|
||||
|
||||
.. autodecorator:: require_feature
|
||||
|
||||
.. autodecorator:: require_identity
|
||||
|
||||
.. autodecorator:: require_feature_subset
|
||||
|
||||
.. autodecorator:: skip_with_quirk
|
||||
|
||||
General decorators
|
||||
------------------
|
||||
|
||||
.. autodecorator:: blocking()
|
||||
|
||||
.. autodecorator:: blocking_timed()
|
||||
|
||||
.. autodecorator:: blocking_with_timeout
|
||||
|
||||
Class for test cases
|
||||
--------------------
|
||||
|
||||
.. autoclass:: TestCase
|
||||
|
||||
.. currentmodule:: aioxmpp.e2etest.provision
|
||||
|
||||
Provisioners
|
||||
============
|
||||
|
||||
.. autoclass:: Provisioner
|
||||
|
||||
.. autoclass:: AnonymousProvisioner()
|
||||
|
||||
.. autoclass:: AnyProvisioner()
|
||||
|
||||
.. autoclass:: StaticPasswordProvisioner()
|
||||
|
||||
.. currentmodule:: aioxmpp.e2etest
|
||||
|
||||
.. autoclass:: Quirk
|
||||
|
||||
.. currentmodule:: aioxmpp.e2etest.provision
|
||||
|
||||
Helper functions
|
||||
----------------
|
||||
|
||||
.. autofunction:: discover_server_features
|
||||
|
||||
.. autofunction:: configure_tls_config
|
||||
|
||||
.. autofunction:: configure_quirks
|
||||
""" # NOQA: E501
|
||||
import asyncio
|
||||
import configparser
|
||||
import functools
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import pytest
|
||||
|
||||
from ..testutils import get_timeout
|
||||
from .utils import blocking
|
||||
from .provision import Quirk # NOQA: F401
|
||||
|
||||
|
||||
provisioner = None
|
||||
config = None
|
||||
only_e2etest = False
|
||||
e2etest_record = None
|
||||
timeout = get_timeout(1.0)
|
||||
|
||||
|
||||
def require_feature(feature_var, argname=None, *, multiple=False):
|
||||
"""
|
||||
:param feature_var: :xep:`30` feature ``var`` of the required feature
|
||||
:type feature_var: :class:`str`
|
||||
:param argname: Optional argument name to pass the :class:`FeatureInfo` to
|
||||
:type argname: :class:`str` or :data:`None`
|
||||
:param multiple: If true, all peers are returned instead of a random one.
|
||||
:type multiple: :class:`bool`
|
||||
|
||||
Before running the function, it is tested that the feature specified by
|
||||
`feature_var` is provided in the environment of the current provisioner. If
|
||||
it is not, :class:`unittest.SkipTest` is raised to skip the test.
|
||||
|
||||
If the feature is available, the :class:`FeatureInfo` instance is passed to
|
||||
the decorated function. If `argname` is :data:`None`, the feature info is
|
||||
passed as additional positional argument. otherwise, it is passed as
|
||||
keyword argument using the `argname`.
|
||||
|
||||
If `multiple` is true, all peers supporting the given feature are passed
|
||||
in a set. Otherwise, only a random peer is returned.
|
||||
|
||||
This decorator can be used on test methods, but not on test classes. If you
|
||||
want to skip all tests in a class, apply the decorator to the ``setUp``
|
||||
method.
|
||||
"""
|
||||
if isinstance(feature_var, str):
|
||||
feature_var = [feature_var]
|
||||
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
global provisioner
|
||||
if multiple:
|
||||
arg = provisioner.get_feature_providers(feature_var)
|
||||
has_provider = bool(arg)
|
||||
else:
|
||||
arg = provisioner.get_feature_provider(feature_var)
|
||||
has_provider = arg is not None
|
||||
if not has_provider:
|
||||
raise unittest.SkipTest(
|
||||
"provisioner does not provide a peer with "
|
||||
"{!r}".format(feature_var)
|
||||
)
|
||||
|
||||
if argname is None:
|
||||
args = args+(arg,)
|
||||
else:
|
||||
kwargs[argname] = arg
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def require_identity(category, type_, argname=None):
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
global provisioner
|
||||
arg = provisioner.get_identity_provider(category, type_)
|
||||
has_provider = arg is not None
|
||||
if not has_provider:
|
||||
raise unittest.SkipTest(
|
||||
"provisioner does not provide a peer with a "
|
||||
"{!r} identity".format((category, type_))
|
||||
)
|
||||
|
||||
if argname is None:
|
||||
args = args+(arg,)
|
||||
else:
|
||||
kwargs[argname] = arg
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def require_feature_subset(feature_vars, required_subset=[]):
|
||||
required_subset = set(required_subset)
|
||||
feature_vars = set(feature_vars) | required_subset
|
||||
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
global provisioner
|
||||
jid, subset = provisioner.get_feature_subset_provider(
|
||||
feature_vars,
|
||||
required_subset
|
||||
)
|
||||
if jid is None:
|
||||
raise unittest.SkipTest(
|
||||
"no peer could provide a subset of {!r} with at least "
|
||||
"{!r}".format(
|
||||
feature_vars,
|
||||
required_subset,
|
||||
)
|
||||
)
|
||||
|
||||
return f(*(args+(jid, feature_vars)),
|
||||
**kwargs)
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def require_pep(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
global provisioner
|
||||
if not provisioner.has_pep():
|
||||
raise unittest.SkipTest(
|
||||
"the provisioned account does not support PEP",
|
||||
)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def skip_with_quirk(quirk):
|
||||
"""
|
||||
:param quirk: The quirk to skip on
|
||||
:type quirk: :class:`Quirks`
|
||||
|
||||
If the provisioner indicates that the environment has the given `quirk`,
|
||||
the test is skipped.
|
||||
|
||||
This decorator can be used on test methods, but not on test classes. If you
|
||||
want to skip all tests in a class, apply the decorator to the ``setUp``
|
||||
method.
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
global provisioner
|
||||
if provisioner.has_quirk(quirk):
|
||||
raise unittest.SkipTest(
|
||||
"provisioner has quirk {!r}".format(quirk)
|
||||
)
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def blocking_with_timeout(timeout):
|
||||
"""
|
||||
The decorated coroutine function is run using the
|
||||
:meth:`~asyncio.AbstractEventLoop.run_until_complete` method of the current
|
||||
(at the time of call) event loop.
|
||||
|
||||
If the execution takes longer than `timeout` seconds,
|
||||
:class:`asyncio.TimeoutError` is raised.
|
||||
|
||||
The decorated function behaves like a normal function and is not a
|
||||
coroutine function.
|
||||
|
||||
This decorator must be applied to a coroutine function (or method).
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
@blocking
|
||||
@functools.wraps(f)
|
||||
async def wrapper(*args, **kwargs):
|
||||
return await asyncio.wait_for(f(*args, **kwargs), timeout)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def blocking_timed(f):
|
||||
"""
|
||||
Like :func:`blocking_with_timeout`, the decorated coroutine function is
|
||||
executed using :meth:`asyncio.AbstractEventLoop.run_until_complete` with a
|
||||
timeout, but the timeout is configured in the end-to-end test configuration
|
||||
(see :ref:`dg-end-to-end-tests`).
|
||||
|
||||
This is the recommended decorator for any test function or method, to
|
||||
prevent the tests from hanging when anythin goes wrong. The timeout is
|
||||
under control of the provisioner configuration, which means that it can be
|
||||
adapted to different setups (for example, running against an XMPP server in
|
||||
the internet will be slower than if it runs on localhost).
|
||||
|
||||
The decorated function behaves like a normal function and is not a
|
||||
coroutine function.
|
||||
|
||||
This decorator must be applied to a coroutine function (or method).
|
||||
"""
|
||||
@blocking
|
||||
@functools.wraps(f)
|
||||
async def wrapper(*args, **kwargs):
|
||||
global timeout
|
||||
await asyncio.wait_for(f(*args, **kwargs), timeout)
|
||||
return wrapper
|
||||
|
||||
|
||||
@blocking
|
||||
async def setup_package():
|
||||
global provisioner, config, timeout
|
||||
if config is None:
|
||||
return
|
||||
|
||||
timeout = config.getfloat("global", "timeout", fallback=timeout)
|
||||
|
||||
provisioner_name = config.get("global", "provisioner")
|
||||
module_path, class_name = provisioner_name.rsplit(".", 1)
|
||||
mod = importlib.import_module(module_path)
|
||||
cls_ = getattr(mod, class_name)
|
||||
|
||||
section = config[provisioner_name]
|
||||
provisioner = cls_()
|
||||
provisioner.configure(section)
|
||||
await provisioner.initialise()
|
||||
|
||||
|
||||
def teardown_package():
|
||||
global provisioner, config
|
||||
if config is None:
|
||||
return
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(provisioner.finalise())
|
||||
loop.close()
|
||||
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
"""
|
||||
A subclass of :class:`unittest.TestCase` for end-to-end test cases.
|
||||
|
||||
This subclass provides a single additional attribute:
|
||||
|
||||
.. autoattribute:: provisioner
|
||||
"""
|
||||
|
||||
__unittest_skip__ = True
|
||||
__unittest_skip_why__ = "this is not the aioxmpp test runner"
|
||||
|
||||
@property
|
||||
def provisioner(self):
|
||||
"""
|
||||
This is the configured :class:`.provision.Provisioner` instance.
|
||||
|
||||
If no provisioner is configured (for example because the e2etest nose
|
||||
plugin is not loaded), this reads as :data:`None`.
|
||||
|
||||
.. note::
|
||||
|
||||
Under nosetests and the vanilla unittest runner, tests inheriting
|
||||
from :class:`TestCase` are automatically skipped if
|
||||
:attr:`provisioner` is :data:`None`.
|
||||
"""
|
||||
global provisioner
|
||||
return provisioner
|
||||
|
||||
|
||||
def pytest_load_initial_conftests(early_config, parser, args):
|
||||
parser.addoption(
|
||||
"--e2etest-config",
|
||||
dest="aioxmpp_e2e_config",
|
||||
default=".local/e2etest.ini",
|
||||
metavar="FILE",
|
||||
help="Configuration file for end-to-end tests "
|
||||
"(default: .local/e2etest.ini)",
|
||||
)
|
||||
parser.addoption(
|
||||
"--e2etest-record",
|
||||
dest="aioxmpp_e2e_record",
|
||||
metavar="FILE",
|
||||
default=None,
|
||||
help="A file to write a transcript to"
|
||||
)
|
||||
parser.addoption(
|
||||
"--e2etest-only",
|
||||
dest="aioxmpp_e2e_only",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="If set, only E2E tests will be executed."
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line("markers", "aioxmpp_e2etest: end-to-end test")
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
return _pytest_cmdline_main_impl(config)
|
||||
|
||||
|
||||
def _pytest_cmdline_main_impl(pytest_config):
|
||||
global config, only_e2etest, e2etest_record
|
||||
config = configparser.ConfigParser()
|
||||
with open(pytest_config.option.aioxmpp_e2e_config, "r") as f:
|
||||
config.read_file(f)
|
||||
|
||||
e2etest_record = pytest_config.option.aioxmpp_e2e_record
|
||||
only_e2etest = pytest_config.option.aioxmpp_e2e_only
|
||||
TestCase.__unittest_skip__ = False
|
||||
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
setup_package()
|
||||
|
||||
|
||||
def pytest_sessionfinish(session):
|
||||
teardown_package()
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
global config, only_e2etest
|
||||
outcome = yield
|
||||
item = outcome.get_result()
|
||||
if isinstance(obj, type) and issubclass(obj, TestCase):
|
||||
if config is None:
|
||||
item.add_marker(pytest.mark.skip("e2e tests not enabled"))
|
||||
else:
|
||||
item.add_marker("aioxmpp_e2etest")
|
||||
elif isinstance(obj, type) and issubclass(obj, unittest.TestCase):
|
||||
if only_e2etest:
|
||||
item.add_marker(pytest.mark.skip("only e2e tests enabled"))
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
global provisioner, e2etest_record
|
||||
if item.get_closest_marker("aioxmpp_e2etest") is not None:
|
||||
blocking(provisioner.setup)()
|
||||
|
||||
|
||||
def pytest_runtest_call(item):
|
||||
if e2etest_record:
|
||||
handler = logging.FileHandler(
|
||||
e2etest_record, "w",
|
||||
)
|
||||
handler.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter(
|
||||
"%(name)s: %(levelname)s: %(message)s",
|
||||
style="%"
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
logger = logging.getLogger("aioxmpp.e2etest.provision")
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
def pytest_runtest_teardown(item):
|
||||
global provisioner
|
||||
if item.get_closest_marker("aioxmpp_e2etest") is not None:
|
||||
blocking(provisioner.teardown)()
|
||||
@@ -0,0 +1,29 @@
|
||||
########################################################################
|
||||
# File name: __main__.py
|
||||
# This file is part of: aioxmpp
|
||||
#
|
||||
# LICENSE
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program. If not, see
|
||||
# <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
########################################################################
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
os.chdir(str(pathlib.Path(__file__).parent.parent.parent))
|
||||
os.execv(
|
||||
sys.executable,
|
||||
[sys.executable, "-m", "pytest", "-p", "aioxmpp.e2etest"] + sys.argv[1:],
|
||||
)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,810 @@
|
||||
########################################################################
|
||||
# File name: provision.py
|
||||
# This file is part of: aioxmpp
|
||||
#
|
||||
# LICENSE
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program. If not, see
|
||||
# <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
########################################################################
|
||||
import abc
|
||||
import ast
|
||||
import asyncio
|
||||
import base64
|
||||
import enum
|
||||
import fnmatch
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import unittest
|
||||
|
||||
import aioxmpp
|
||||
import aioxmpp.disco
|
||||
import aioxmpp.security_layer
|
||||
import aioxmpp.connector
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_rng = random.SystemRandom()
|
||||
|
||||
|
||||
class Quirk(enum.Enum):
|
||||
"""
|
||||
Enumeration of implementation quirks.
|
||||
|
||||
Each enumeration member represents a quirk of an implementation. A quirk is
|
||||
a behaviour of an implementation which does not directly violate standards,
|
||||
but which is unfortunate in a way that it disables some features of
|
||||
:mod:`aioxmpp`.
|
||||
|
||||
One example of such a quirk is the rewriting of message stanza IDs which
|
||||
some MUC implementations do when reflecting the messages. This breaks the
|
||||
stanza tracking of :meth:`aioxmpp.muc.Room.send_tracked_message`.
|
||||
|
||||
The following quirks are defined:
|
||||
|
||||
.. attribute:: MUC_REWRITES_MESSAGE_ID
|
||||
:annotation: https://zombofant.net/xmlns/aioxmpp/e2etest/quirks#muc-id-rewrite
|
||||
|
||||
This quirk must be configured when the environment the provisioner
|
||||
provides rewrites the message IDs when they are reflected by the MUC
|
||||
implementation.
|
||||
|
||||
The quirk does not need to be set if the environment does not provide a
|
||||
MUC implementation at all.
|
||||
|
||||
.. attribute:: PUBSUB_GET_ITEMS_BY_ID_BROKEN
|
||||
:annotation: https://zombofant.net/xmlns/aioxmpp/e2etest/quirks#broken-pubsub-get-multiple-by-id
|
||||
|
||||
Indicates that the "Get Items by Id" operation in the PubSub service used
|
||||
is broken when more than one item is requested.
|
||||
""" # NOQA: E501
|
||||
|
||||
MUC_REWRITES_MESSAGE_ID = \
|
||||
"https://zombofant.net/xmlns/aioxmpp/e2etest/quirks#muc-id-rewrite"
|
||||
NO_ADHOC_PING = \
|
||||
"https://zombofant.net/xmlns/aioxmpp/e2etest/quirks#no-adhoc-ping"
|
||||
MUC_NO_333 = \
|
||||
"https://zombofant.net/xmlns/aioxmpp/e2etest/quirks#muc-no-333"
|
||||
BROKEN_MUC = \
|
||||
"https://zombofant.net/xmlns/aioxmpp/e2etest/quirks#broken-muc"
|
||||
PUBSUB_GET_MULTIPLE_ITEMS_BY_ID_BROKEN = \
|
||||
"https://zombofant.net/xmlns/aioxmpp/e2etest/quirks#broken-pubsub-get-multiple-by-id" # NOQA: E501
|
||||
NO_PRIVATE_XML = \
|
||||
"https://zombofant.net/xmlns/aioxmpp/e2etest/quirks#no-xep-0049"
|
||||
|
||||
|
||||
def fix_quirk_str(s):
|
||||
if s.startswith("#"):
|
||||
return "https://zombofant.net/xmlns/aioxmpp/e2etest/quirks" + s
|
||||
return s
|
||||
|
||||
|
||||
def configure_tls_config(section):
|
||||
"""
|
||||
Generate keyword arguments for use with :meth:`.security_layer.make` from
|
||||
the configuration which control the TLS behaviour of the security layer.
|
||||
|
||||
:param section: Configuration section to work on.
|
||||
:return: Keyword arguments for :meth:`.security_layer.make`
|
||||
:rtype: :class:`dict`
|
||||
|
||||
The generated keyword arguments are ``pin_type``, ``pin_store`` and
|
||||
``no_verify``. The options in the config file have the same names and the
|
||||
semantics are the following:
|
||||
|
||||
``pin_store`` and ``pin_type`` can be used to configure certificate
|
||||
pinning, in case the server you want to test against does not have a
|
||||
certificate which passes the default OpenSSL PKIX tests.
|
||||
|
||||
If set, ``pin_store`` must point to a JSON file, which consists of a single
|
||||
object mapping host names to arrays of strings containing the base64
|
||||
representation of what is being pinned. This is determined by ``pin_type``,
|
||||
which can be ``0`` for Public Key pinning and ``1`` for Certificate
|
||||
pinning.
|
||||
|
||||
There is also the ``no_verify`` option, which, if set to true, will disable
|
||||
certificate verification altogether. This does not much harm if you are
|
||||
testing against localhost anyways and saves the configuration nuisance for
|
||||
certificate pinning. ``no_verfiy`` takes precedence over ``pin_store`` and
|
||||
``pin_type``.
|
||||
"""
|
||||
|
||||
no_verify = section.getboolean(
|
||||
"no_verify",
|
||||
fallback=False
|
||||
)
|
||||
|
||||
if not no_verify and "pin_store" in section:
|
||||
with open(section.get("pin_store")) as f:
|
||||
pin_store = json.load(f)
|
||||
pin_type = aioxmpp.security_layer.PinType(
|
||||
section.getint("pin_type", fallback=0)
|
||||
)
|
||||
else:
|
||||
pin_store = None
|
||||
pin_type = None
|
||||
|
||||
return {
|
||||
"pin_store": pin_store,
|
||||
"pin_type": pin_type,
|
||||
"no_verify": no_verify,
|
||||
}
|
||||
|
||||
|
||||
def configure_quirks(section):
|
||||
"""
|
||||
Generate a set of :class:`.Quirk` enum members from the given configuration
|
||||
section.
|
||||
|
||||
:param section: Configuration section to work on.
|
||||
:return: Set of :class:`.Quirk` members
|
||||
|
||||
This parses the configuration key ``quirks`` as a python literal (see
|
||||
:func:`ast.literal_eval`). It expects a list of strings as a result.
|
||||
|
||||
The strings are interpreted as :class:`.Quirk` enum values. If a string
|
||||
starts with ``#``, it is prefixed with
|
||||
``https://zombofant.net/xmlns/aioxmpp/e2etest/quirks`` for easier manual
|
||||
writing of the configuration. See :class:`.Quirk` for the currently defined
|
||||
quirks.
|
||||
"""
|
||||
|
||||
quirks = ast.literal_eval(section.get("quirks", fallback="[]"))
|
||||
if isinstance(quirks, (str, dict)):
|
||||
raise ValueError("incorrect type for quirks setting")
|
||||
return set(map(Quirk, map(fix_quirk_str, quirks)))
|
||||
|
||||
|
||||
def configure_blockmap(section):
|
||||
blockmap_raw = ast.literal_eval(section.get("block_features",
|
||||
fallback="{}"))
|
||||
return {
|
||||
aioxmpp.JID.fromstr(entity): features
|
||||
for entity, features in blockmap_raw.items()
|
||||
}
|
||||
|
||||
|
||||
def _is_feature_blocked(peer, feature, blockmap):
|
||||
return any(
|
||||
fnmatch.fnmatch(feature, item)
|
||||
for item in blockmap.get(peer, [])
|
||||
)
|
||||
|
||||
|
||||
async def discover_server_features(disco, peer, recurse_into_items=True,
|
||||
blockmap={}):
|
||||
"""
|
||||
Use :xep:`30` service discovery to discover features supported by the
|
||||
server.
|
||||
|
||||
:param disco: Service discovery client which can query the `peer` server.
|
||||
:type disco: :class:`aioxmpp.DiscoClient`
|
||||
:param peer: The JID of the server to query
|
||||
:type peer: :class:`~aioxmpp.JID`
|
||||
:param recurse_into_items: If set to true, the :xep:`30` items exposed by
|
||||
the server will also be queried for their
|
||||
features. Only one level of recursion is
|
||||
performed.
|
||||
:return: A mapping which maps :xep:`30` feature vars to the JIDs at which
|
||||
the service is provided.
|
||||
|
||||
This uses :xep:`30` service discovery to obtain a set of features supported
|
||||
at `peer`. The set of features is returned as a mapping which maps the
|
||||
``var`` values of the features to the JID at which they were discovered.
|
||||
|
||||
If `recurse_into_items` is true, a :xep:`30` items query is run against
|
||||
`peer`. For each JID discovered that way, :func:`discover_server_features`
|
||||
is re-invoked (with `recurse_into_items` set to false). The resulting
|
||||
mappings are merged with the mapping obtained from querying the features of
|
||||
`peer` (existing entries are *not* overridden -- so `peer` takes
|
||||
precedence).
|
||||
"""
|
||||
|
||||
server_info = await disco.query_info(peer)
|
||||
|
||||
all_features = {
|
||||
feature: [peer]
|
||||
for feature in server_info.features
|
||||
if not _is_feature_blocked(peer, feature, blockmap)
|
||||
}
|
||||
|
||||
if recurse_into_items:
|
||||
server_items = await disco.query_items(peer)
|
||||
features_list = await asyncio.gather(
|
||||
*(
|
||||
discover_server_features(
|
||||
disco,
|
||||
item.jid,
|
||||
recurse_into_items=False,
|
||||
)
|
||||
for item in server_items.items
|
||||
if item.jid is not None and item.node is None
|
||||
)
|
||||
)
|
||||
|
||||
for features in features_list:
|
||||
for feature, providers in features.items():
|
||||
all_features.setdefault(feature, []).extend(providers)
|
||||
|
||||
return all_features
|
||||
|
||||
|
||||
async def discover_server_identities(disco, peer, recurse_into_items=True):
|
||||
"""
|
||||
Use :xep:`30` service discovery to discover identities provided by the
|
||||
server.
|
||||
|
||||
:param disco: Service discovery client which can query the `peer` server.
|
||||
:type disco: :class:`aioxmpp.DiscoClient`
|
||||
:param peer: The JID of the server to query
|
||||
:type peer: :class:`~aioxmpp.JID`
|
||||
:param recurse_into_items: If set to true, the :xep:`30` items exposed by
|
||||
the server will also be queried for their
|
||||
identities. Only one level of recursion is
|
||||
performed.
|
||||
:return: A mapping which maps :xep:`30` (category, type) tuples to the
|
||||
JIDs at which the identity is provided.
|
||||
|
||||
This uses :xep:`30` service discovery to obtain a set of identities offered
|
||||
at `peer`. The set of identities is returned as a mapping which maps the
|
||||
``(category, type)`` tuples of the identities to the JID at which they were
|
||||
discovered.
|
||||
|
||||
If `recurse_into_items` is true, a :xep:`30` items query is run against
|
||||
`peer`. For each JID discovered that way,
|
||||
:func:`discover_server_identities` is re-invoked (with `recurse_into_items`
|
||||
set to false). The resulting mappings are merged with the mapping obtained
|
||||
from querying the identities of `peer` (existing entries are *not*
|
||||
overridden -- so `peer` takes precedence).
|
||||
"""
|
||||
|
||||
server_info = await disco.query_info(peer)
|
||||
|
||||
all_identities = {
|
||||
(identity.category, identity.type_): [peer]
|
||||
for identity in server_info.identities
|
||||
}
|
||||
|
||||
if recurse_into_items:
|
||||
server_items = await disco.query_items(peer)
|
||||
identities_list = await asyncio.gather(
|
||||
*(
|
||||
discover_server_identities(
|
||||
disco,
|
||||
item.jid,
|
||||
recurse_into_items=False,
|
||||
)
|
||||
for item in server_items.items
|
||||
if item.jid is not None and item.node is None
|
||||
)
|
||||
)
|
||||
|
||||
for identities in identities_list:
|
||||
for identity, providers in identities.items():
|
||||
all_identities.setdefault(identity, []).extend(providers)
|
||||
|
||||
return all_identities
|
||||
|
||||
|
||||
class Provisioner(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
Base class for provisioners.
|
||||
|
||||
Provisioners are responsible for providing test cases with XMPP accounts
|
||||
and client objects connected to these accounts, as well as information
|
||||
about the environment the accounts live in.
|
||||
|
||||
A provisioner must implement the following methods:
|
||||
|
||||
.. automethod:: _make_client
|
||||
|
||||
.. automethod:: configure
|
||||
|
||||
The following methods are the API used by test cases:
|
||||
|
||||
.. automethod:: get_connected_client
|
||||
|
||||
.. automethod:: get_feature_provider
|
||||
|
||||
.. automethod:: get_identity_provider
|
||||
|
||||
.. automethod:: has_quirk
|
||||
|
||||
These methods can be used by provisioners to perform plumbing tasks, such
|
||||
as shutting down clients or deleting accounts:
|
||||
|
||||
.. automethod:: initialise
|
||||
|
||||
.. automethod:: finalise
|
||||
|
||||
.. automethod:: setup
|
||||
|
||||
.. automethod:: teardown
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, logger=_logger):
|
||||
super().__init__()
|
||||
self._accounts_to_dispose = []
|
||||
self._featuremap = {}
|
||||
self._identitymap = {}
|
||||
self._account_info = None
|
||||
self._logger = logger
|
||||
self.__counter = 0
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _make_client(self, logger):
|
||||
"""
|
||||
:param logger: The logger to pass to the client.
|
||||
:return: Client with a fresh account.
|
||||
|
||||
Construct a new :class:`aioxmpp.PresenceManagedClient` connected to a
|
||||
new account. This method must be re-implemented by subclasses.
|
||||
"""
|
||||
|
||||
async def get_connected_client(self, presence=aioxmpp.PresenceState(True), *,
|
||||
services=[], prepare=None):
|
||||
"""
|
||||
Return a connected client to a unique XMPP account.
|
||||
|
||||
:param presence: initial presence to emit
|
||||
:type presence: :class:`aioxmpp.PresenceState`
|
||||
:param prepare: a coroutine run after the services
|
||||
are summoned but before the client connects.
|
||||
:type prepare: coroutine receiving the client
|
||||
as argument
|
||||
:raise OSError: if the connection failed
|
||||
:raise RuntimeError: if a client could not be provisioned due to
|
||||
resource constraints
|
||||
:return: Connected presence managed client
|
||||
:rtype: :class:`aioxmpp.PresenceManagedClient`
|
||||
|
||||
Each account used by the clients returned from this method is unique;
|
||||
all clients are guaranteed to have different bare JIDs.
|
||||
|
||||
The clients and accounts are cleaned up after the tear down of the test
|
||||
runs. Some provisioners may have a limit on the number of accounts
|
||||
which can be used in the same test.
|
||||
|
||||
Clients obtained from this function are cleaned up automatically on
|
||||
tear down of the test. The clients are stopped and the accounts
|
||||
deleted or cleared, so that each test starts with a fully fresh state.
|
||||
|
||||
A coroutine may be passed as `prepare` argument. It is called
|
||||
with the client as the single argument after all services in
|
||||
`services` have been summoned but before the client connects,
|
||||
this is for example useful to connect signals that fire early
|
||||
in the connection process.
|
||||
"""
|
||||
id_ = self.__counter
|
||||
self.__counter += 1
|
||||
self._logger.debug("obtaining client%d from %r", id_, self)
|
||||
logger = self._logger.getChild("client{}".format(id_))
|
||||
client = await self._make_client(logger)
|
||||
for service in services:
|
||||
client.summon(service)
|
||||
if prepare is not None:
|
||||
await prepare(client)
|
||||
cm = client.connected(presence=presence)
|
||||
await cm.__aenter__()
|
||||
self._accounts_to_dispose.append(cm)
|
||||
return client
|
||||
|
||||
def get_feature_providers(self, feature_nses):
|
||||
"""
|
||||
:param feature_ns: Namespace URIs to find a provider for
|
||||
:type feature_ns: iterable of :class:`str`
|
||||
:return: JIDs of the entities providing all features
|
||||
:rtype: :class:`set` of :class:`aioxmpp.JID`
|
||||
|
||||
If there is no entity supporting all requested features, the empty set
|
||||
is returned.
|
||||
"""
|
||||
providers = set()
|
||||
iterator = iter(feature_nses)
|
||||
try:
|
||||
first_ns = next(iterator)
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
providers = set(self._featuremap.get(first_ns, []))
|
||||
for feature_ns in iterator:
|
||||
providers &= set(self._featuremap.get(feature_ns, []))
|
||||
return providers
|
||||
|
||||
def get_feature_provider(self, feature_nses):
|
||||
"""
|
||||
:param feature_ns: Namespace URIs to find a provider for
|
||||
:type feature_ns: iterable of :class:`str`
|
||||
:return: JID of the entity providing all features
|
||||
:rtype: :class:`aioxmpp.JID`
|
||||
|
||||
If there is no entity supporting all requested features, :data:`None`
|
||||
is returned.
|
||||
"""
|
||||
providers = self.get_feature_providers(feature_nses)
|
||||
if not providers:
|
||||
return None
|
||||
return next(iter(providers))
|
||||
|
||||
def get_identity_provider(self, category, type_):
|
||||
return next(iter(self._identitymap.get((category, type_), [])))
|
||||
|
||||
def get_feature_subset_provider(self, feature_nses, required_subset):
|
||||
required_subset = set(required_subset)
|
||||
|
||||
candidates = {}
|
||||
for feature_ns in feature_nses:
|
||||
providers = self._featuremap.get(feature_ns, [])
|
||||
for provider in providers:
|
||||
candidates.setdefault(provider, set()).add(feature_ns)
|
||||
|
||||
candidates = sorted(
|
||||
(
|
||||
(provider, features)
|
||||
for provider, features in candidates.items()
|
||||
if features & required_subset == required_subset
|
||||
),
|
||||
key=lambda x: (len(x[1]))
|
||||
)
|
||||
|
||||
try:
|
||||
return candidates.pop()
|
||||
except IndexError:
|
||||
return None, None
|
||||
|
||||
def has_quirk(self, quirk):
|
||||
"""
|
||||
:param quirk: Quirk to check for
|
||||
:type quirk: :class:`Quirk`
|
||||
:return: true if the environment has the given quirk
|
||||
"""
|
||||
return quirk in self._quirks
|
||||
|
||||
def has_pep(self):
|
||||
"""
|
||||
:return: true if the account has PEP support, false otherwise.
|
||||
"""
|
||||
if not self._account_info:
|
||||
return False
|
||||
return any(ident.category == "pubsub" and ident.type_ == "pep"
|
||||
for ident in self._account_info.identities)
|
||||
|
||||
@abc.abstractmethod
|
||||
def configure(self, section):
|
||||
"""
|
||||
Read the configuration and set up the provisioner.
|
||||
|
||||
:param section: mapping of config keys to values
|
||||
|
||||
Subclasses will implement this to configure their account setup and
|
||||
servers to use.
|
||||
|
||||
.. seealso::
|
||||
:func:`configure_tls_config`
|
||||
for a function which extracts TLS-related arguments for
|
||||
:func:`aioxmpp.security_layer.make`
|
||||
:func:`configure_quirks`
|
||||
for a function which extracts a set of :class:`.Quirk`
|
||||
enumeration members from the configuration
|
||||
:func:`configure_blockmap`
|
||||
for a function which extracts a mapping which allows to block
|
||||
features from specific hosts
|
||||
"""
|
||||
|
||||
async def initialise(self):
|
||||
"""
|
||||
Called once on test framework startup.
|
||||
|
||||
Subclasses may run service discovery code here to detect features of
|
||||
the environment they are connected to.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:func:`discover_server_features`
|
||||
for a function which uses :xep:`30` service discovery to find
|
||||
features.
|
||||
"""
|
||||
|
||||
async def finalise(self):
|
||||
"""
|
||||
Called once on test framework shutdown (timeout of 10 seconds applies).
|
||||
"""
|
||||
|
||||
async def setup(self):
|
||||
"""
|
||||
Called before each test run.
|
||||
"""
|
||||
|
||||
async def teardown(self):
|
||||
"""
|
||||
Called after each test run.
|
||||
|
||||
The default implementation cleans up the clients obtained from
|
||||
:meth:`get_connected_client`.
|
||||
"""
|
||||
|
||||
futures = []
|
||||
for cm in self._accounts_to_dispose:
|
||||
futures.append(asyncio.ensure_future(
|
||||
cm.__aexit__(None, None, None)
|
||||
))
|
||||
|
||||
self._accounts_to_dispose.clear()
|
||||
|
||||
self._logger.debug("waiting for %d accounts to shut down",
|
||||
len(futures))
|
||||
await asyncio.gather(
|
||||
*futures,
|
||||
return_exceptions=True
|
||||
)
|
||||
|
||||
|
||||
class _AutoConfiguredProvisioner(Provisioner):
|
||||
def configure(self, section):
|
||||
super().configure(section)
|
||||
self._blockmap = configure_blockmap(section)
|
||||
|
||||
async def initialise(self):
|
||||
self._logger.debug("auto-configuring provisioner %s", self)
|
||||
|
||||
client = await self.get_connected_client()
|
||||
disco = client.summon(aioxmpp.DiscoClient)
|
||||
|
||||
self._featuremap.update(await discover_server_features(
|
||||
disco,
|
||||
self._domain,
|
||||
blockmap=self._blockmap,
|
||||
))
|
||||
|
||||
self._identitymap.update(await discover_server_identities(
|
||||
disco,
|
||||
self._domain,
|
||||
))
|
||||
|
||||
self._logger.debug("found %d features", len(self._featuremap))
|
||||
if self._logger.isEnabledFor(logging.DEBUG):
|
||||
for feature, providers in self._featuremap.items():
|
||||
self._logger.debug(
|
||||
"%s provided by %s",
|
||||
feature,
|
||||
", ".join(sorted(map(str, providers)))
|
||||
)
|
||||
|
||||
self._account_info = await disco.query_info(None)
|
||||
|
||||
# clean up state
|
||||
del client
|
||||
await self.teardown()
|
||||
|
||||
|
||||
class AnonymousProvisioner(_AutoConfiguredProvisioner):
|
||||
"""
|
||||
This provisioner uses SASL ANONYMOUS to obtain accounts.
|
||||
|
||||
It is dead-simple to configure: it needs a host to connect to, and
|
||||
optionally some TLS and quirks configuration. The host is specified as
|
||||
configuration key ``host``, TLS can be configured as documented in
|
||||
:func:`configure_tls_config` and quirks are set as described in
|
||||
:func:`configure_quirks`. A configuration for a locally running Prosody
|
||||
instance might look like this:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[aioxmpp.e2etest.provision.AnonymousProvisioner]
|
||||
host=localhost
|
||||
no_verify=true
|
||||
quirks=[]
|
||||
|
||||
The server configured in ``host`` must support SASL ANONYMOUS and must
|
||||
allow communication between the clients connected that way. It may provide
|
||||
PubSub and/or MUC services, which will be auto-discovered if they are
|
||||
provided in the :xep:`30` items of the server.
|
||||
"""
|
||||
|
||||
def configure(self, section):
|
||||
super().configure(section)
|
||||
self.__host = section.get("host")
|
||||
self._domain = aioxmpp.JID.fromstr(section.get(
|
||||
"domain",
|
||||
self.__host
|
||||
))
|
||||
self.__port = section.getint("port")
|
||||
self.__security_layer = aioxmpp.make_security_layer(
|
||||
None,
|
||||
anonymous="",
|
||||
**configure_tls_config(
|
||||
section
|
||||
)
|
||||
)
|
||||
self._quirks = configure_quirks(section)
|
||||
|
||||
async def _make_client(self, logger):
|
||||
override_peer = []
|
||||
if self.__port is not None:
|
||||
override_peer.append(
|
||||
(self.__host, self.__port,
|
||||
aioxmpp.connector.STARTTLSConnector())
|
||||
)
|
||||
|
||||
return aioxmpp.PresenceManagedClient(
|
||||
self._domain,
|
||||
self.__security_layer,
|
||||
override_peer=override_peer,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
|
||||
class AnyProvisioner(_AutoConfiguredProvisioner):
|
||||
"""
|
||||
This provisioner randomly generates usernames and uses a hardcoded password
|
||||
to authenticate with the XMPP server.
|
||||
|
||||
This is for use with ``mod_auth_any`` of prosody.
|
||||
|
||||
It is dead-simple to configure: it needs a host to connect to, and
|
||||
optionally some TLS and quirks configuration. The host is specified as
|
||||
configuration key ``host``, TLS can be configured as documented in
|
||||
:func:`configure_tls_config` and quirks are set as described in
|
||||
:func:`configure_quirks`. A configuration for a locally running Prosody
|
||||
instance might look like this:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[aioxmpp.e2etest.provision.AnyProvisioner]
|
||||
host=localhost
|
||||
no_verify=true
|
||||
quirks=[]
|
||||
|
||||
The server configured in ``host`` must allow authentication with any
|
||||
username/password pair and allow communication between the clients
|
||||
connected that way. It may provide PubSub and/or MUC services, which will
|
||||
be auto-discovered if they are provided in the :xep:`30` items of the
|
||||
server.
|
||||
"""
|
||||
|
||||
def configure(self, section):
|
||||
super().configure(section)
|
||||
self.__host = section.get("host")
|
||||
self._domain = aioxmpp.JID.fromstr(section.get(
|
||||
"domain",
|
||||
self.__host
|
||||
))
|
||||
self.__port = section.getint("port")
|
||||
self.__security_layer = aioxmpp.make_security_layer(
|
||||
"foobar2342", # password is irrelevant, but must be given.
|
||||
**configure_tls_config(
|
||||
section
|
||||
)
|
||||
)
|
||||
self._quirks = configure_quirks(section)
|
||||
self.__username_rng = random.Random()
|
||||
self.__username_rng.seed(_rng.getrandbits(256))
|
||||
|
||||
async def _make_client(self, logger):
|
||||
override_peer = []
|
||||
if self.__port is not None:
|
||||
override_peer.append(
|
||||
(self.__host, self.__port,
|
||||
aioxmpp.connector.STARTTLSConnector())
|
||||
)
|
||||
|
||||
user = base64.b32encode(
|
||||
self.__username_rng.getrandbits(128).to_bytes(128//8, 'little')
|
||||
).decode("ascii").rstrip("=")
|
||||
user_jid = self._domain.replace(localpart=user)
|
||||
|
||||
return aioxmpp.PresenceManagedClient(
|
||||
user_jid,
|
||||
self.__security_layer,
|
||||
override_peer=override_peer,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
|
||||
class StaticPasswordProvisioner(_AutoConfiguredProvisioner):
|
||||
"""
|
||||
This provisioner expects a list of username/password pairs to authenticate
|
||||
against the tested server.
|
||||
|
||||
This is for use with servers which support neither SASL ANONYMOUS nor
|
||||
a ``mod_auth_any`` equivalent.
|
||||
|
||||
The configuration of this provisioner is slightly unwieldy since we do
|
||||
not want to add a dependency to a more sane configuration file format. Here
|
||||
is an example on how to configure a provisioner with two accounts:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[aioxmpp.e2etest.provision.StaticPasswordProvisioner]
|
||||
host=localhost
|
||||
accounts=[("user1", "password1"), ("user2", "password2")]
|
||||
skip_on_too_few_accounts=false
|
||||
|
||||
All accounts need to have exactly the same privileges on the server. The
|
||||
first account will be used to auto-discover any features offered by the
|
||||
test environment.
|
||||
|
||||
If `skip_on_too_few_accounts` is set to true (the default is false), tests
|
||||
will be skipped if the provisioner runs out of accounts instead of failing.
|
||||
"""
|
||||
|
||||
def _load_accounts(self, cfg):
|
||||
result = []
|
||||
for username, password in ast.literal_eval(cfg):
|
||||
result.append((
|
||||
aioxmpp.JID(localpart=username, domain=self._domain.domain,
|
||||
resource=None),
|
||||
aioxmpp.make_security_layer(password, **self.__tls_config)
|
||||
))
|
||||
return result
|
||||
|
||||
def configure(self, section):
|
||||
super().configure(section)
|
||||
self.__host = section.get("host")
|
||||
self._domain = aioxmpp.JID.fromstr(section.get(
|
||||
"domain",
|
||||
self.__host
|
||||
))
|
||||
self.__port = section.getint("port")
|
||||
self.__tls_config = configure_tls_config(section)
|
||||
self.__accounts = self._load_accounts(section.get("accounts"))
|
||||
if len(self.__accounts) == 0:
|
||||
raise RuntimeError(
|
||||
"at least one account needs to be configured in the "
|
||||
"StaticPasswordProvisioner section"
|
||||
)
|
||||
|
||||
self.__nused_accounts = 0
|
||||
self._quirks = configure_quirks(section)
|
||||
self.__username_rng = random.Random()
|
||||
self.__skip_on_too_few_accounts = section.getboolean(
|
||||
"skip_on_too_few_accounts",
|
||||
fallback=False,
|
||||
)
|
||||
|
||||
async def _make_client(self, logger):
|
||||
override_peer = []
|
||||
if self.__port is not None:
|
||||
override_peer.append(
|
||||
(self.__host, self.__port,
|
||||
aioxmpp.connector.STARTTLSConnector())
|
||||
)
|
||||
|
||||
next_account = self.__nused_accounts
|
||||
try:
|
||||
address, security_layer = self.__accounts[next_account]
|
||||
except IndexError:
|
||||
err = (
|
||||
"not enough accounts; needed at least one more account "
|
||||
"after already using {} accounts".format(next_account)
|
||||
)
|
||||
if self.__skip_on_too_few_accounts:
|
||||
raise unittest.SkipTest(err)
|
||||
raise RuntimeError(err)
|
||||
|
||||
self.__nused_accounts += 1
|
||||
|
||||
return aioxmpp.PresenceManagedClient(
|
||||
address,
|
||||
security_layer,
|
||||
override_peer=override_peer,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
async def teardown(self):
|
||||
await super().teardown()
|
||||
self.__nused_accounts = 0
|
||||
@@ -0,0 +1,42 @@
|
||||
########################################################################
|
||||
# File name: utils.py
|
||||
# This file is part of: aioxmpp
|
||||
#
|
||||
# LICENSE
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program. If not, see
|
||||
# <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
########################################################################
|
||||
import asyncio
|
||||
import functools
|
||||
|
||||
|
||||
def blocking(f):
|
||||
"""
|
||||
The decorated coroutine function is run using the
|
||||
:meth:`~asyncio.AbstractEventLoop.run_until_complete` method of the current
|
||||
(at the time of call) event loop.
|
||||
|
||||
The decorated function behaves like a normal function and is not a
|
||||
coroutine function.
|
||||
|
||||
This decorator must be applied to a coroutine function (or method).
|
||||
"""
|
||||
|
||||
@functools.wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
loop = asyncio.get_event_loop()
|
||||
return loop.run_until_complete(f(*args, **kwargs))
|
||||
return wrapped
|
||||
Reference in New Issue
Block a user