From 3d1e539d2bdecc9763a5d5be86f53da0638e8189 Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 4 Dec 2020 22:59:34 +0100 Subject: [PATCH 1/5] XMLStream: Add a wait_until coroutine It will set a disposable handler on an event and wait on it with a specific timeout. Useful for integration tests without callback hell. --- slixmpp/xmlstream/xmlstream.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/slixmpp/xmlstream/xmlstream.py b/slixmpp/xmlstream/xmlstream.py index af494903..066d84df 100644 --- a/slixmpp/xmlstream/xmlstream.py +++ b/slixmpp/xmlstream/xmlstream.py @@ -12,7 +12,7 @@ :license: MIT, see LICENSE for more details """ -from typing import Optional, Set, Callable +from typing import Optional, Set, Callable, Any import functools import logging @@ -1130,3 +1130,18 @@ class XMLStream(asyncio.BaseProtocol): :param exception: An unhandled exception object. """ pass + + async def wait_until(self, event: str, timeout=30) -> Any: + """Utility method to wake on the next firing of an event. + (Registers a disposable handler on it) + + :param str event: Event to wait on. + :param int timeout: Timeout + """ + fut = asyncio.Future() + self.add_event_handler( + event, + fut.set_result, + disposable=True, + ) + return await asyncio.wait_for(fut, timeout) From e6d1badb81f7bacf78f57b83d0f06271f3e99d9a Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 4 Dec 2020 23:01:54 +0100 Subject: [PATCH 2/5] CI: Add helper for integration tests --- slixmpp/test/integration.py | 61 +++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 slixmpp/test/integration.py diff --git a/slixmpp/test/integration.py b/slixmpp/test/integration.py new file mode 100644 index 00000000..d15019cc --- /dev/null +++ b/slixmpp/test/integration.py @@ -0,0 +1,61 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2020 Mathieu Pasquet + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +import asyncio +import os +try: + from unittest import IsolatedAsyncioTestCase +except ImportError: + # Python < 3.8 + # just to make sure the imports do not break, but + # not usable. + from unittest import TestCase as IsolatedAsyncioTestCase +from typing import ( + List, +) + +from slixmpp import JID +from slixmpp.clientxmpp import ClientXMPP + + +class SlixIntegration(IsolatedAsyncioTestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.clients = [] + self.addAsyncCleanup(self._destroy) + + def envjid(self, name): + """Get a JID from an env var""" + value = os.getenv(name) + return JID(value) + + def envstr(self, name): + """get a str from an env var""" + return os.getenv(name) + + def register_plugins(self, plugins: List[str]): + """Register plugins on all known clients""" + for plugin in plugins: + for client in self.clients: + client.register_plugin(plugin) + + def add_client(self, jid: JID, password: str): + """Register a new client""" + self.clients.append(ClientXMPP(jid, password)) + + async def connect_clients(self): + """Connect all clients""" + for client in self.clients: + client.connect() + await client.wait_until('session_start') + + async def _destroy(self): + """Kill all clients""" + for client in self.clients: + client.abort() From d3dc09ce945e9a66d09b179c55176b7f6a4f78ea Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 4 Dec 2020 23:02:24 +0100 Subject: [PATCH 3/5] CI: add a script to run integration tests (same as run_tests.py, but use adifferent directory) --- run_integration_tests.py | 71 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100755 run_integration_tests.py diff --git a/run_integration_tests.py b/run_integration_tests.py new file mode 100755 index 00000000..9f670b5c --- /dev/null +++ b/run_integration_tests.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +import sys +import logging +import unittest + +from argparse import ArgumentParser +from distutils.core import Command +from importlib import import_module +from pathlib import Path + + +def run_tests(filenames=None): + """ + Find and run all tests in the tests/ directory. + + Excludes live tests (tests/live_*). + """ + if sys.version_info < (3, 8): + raise ValueError('Your python version is too old to run these tests') + if not filenames: + filenames = [i for i in Path('itests').glob('test_*')] + else: + filenames = [Path(i) for i in filenames] + + modules = ['.'.join(test.parts[:-1] + (test.stem,)) for test in filenames] + + suites = [] + for filename in modules: + module = import_module(filename) + suites.append(module.suite) + + tests = unittest.TestSuite(suites) + runner = unittest.TextTestRunner(verbosity=2) + + # Disable logging output + logging.basicConfig(level=100) + logging.disable(100) + + result = runner.run(tests) + return result + + +# Add a 'test' command for setup.py + +class TestCommand(Command): + + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + run_tests() + + +if __name__ == '__main__': + parser = ArgumentParser(description='Run unit tests.') + parser.add_argument('tests', metavar='TEST', nargs='*', help='list of tests to run, or nothing to run them all') + args = parser.parse_args() + + result = run_tests(args.tests) + print("" % ( + "xmlns='http//andyet.net/protocol/tests'", + result.testsRun, len(result.errors), + len(result.failures), result.wasSuccessful())) + + sys.exit(not result.wasSuccessful()) From 2cb2fcefbf3aaed2596d4c49423ad3285fbb277d Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 4 Dec 2020 23:04:49 +0100 Subject: [PATCH 4/5] Add some very basic integration tests --- itests/__init__.py | 0 itests/test_basic_connect_and_message.py | 28 +++++++++ itests/test_muc.py | 78 ++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 itests/__init__.py create mode 100644 itests/test_basic_connect_and_message.py create mode 100644 itests/test_muc.py diff --git a/itests/__init__.py b/itests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/itests/test_basic_connect_and_message.py b/itests/test_basic_connect_and_message.py new file mode 100644 index 00000000..79ad37be --- /dev/null +++ b/itests/test_basic_connect_and_message.py @@ -0,0 +1,28 @@ +import unittest +from slixmpp.test.integration import SlixIntegration + + +class TestConnect(SlixIntegration): + async def asyncSetUp(self): + await super().asyncSetUp() + self.add_client( + self.envjid('CI_ACCOUNT1'), + self.envstr('CI_ACCOUNT1_PASSWORD'), + ) + self.add_client( + self.envjid('CI_ACCOUNT2'), + self.envstr('CI_ACCOUNT2_PASSWORD'), + ) + await self.connect_clients() + + async def test_send_message(self): + """Make sure we can send and receive messages""" + msg = self.clients[0].make_message( + mto=self.clients[1].boundjid, mbody='Msg body', + ) + msg.send() + message = await self.clients[1].wait_until('message') + self.assertEqual(message['body'], msg['body']) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestConnect) diff --git a/itests/test_muc.py b/itests/test_muc.py new file mode 100644 index 00000000..3dc91955 --- /dev/null +++ b/itests/test_muc.py @@ -0,0 +1,78 @@ +import asyncio +import unittest +from uuid import uuid4 +from slixmpp import JID +from slixmpp.test.integration import SlixIntegration + +UNIQUE = uuid4().hex + + +class TestConnect(SlixIntegration): + + async def asyncSetUp(self): + self.mucserver = self.envjid('CI_MUC_SERVER') + self.muc = JID('%s@%s' % (UNIQUE, self.mucserver)) + self.add_client( + self.envjid('CI_ACCOUNT1'), + self.envstr('CI_ACCOUNT1_PASSWORD'), + ) + self.add_client( + self.envjid('CI_ACCOUNT2'), + self.envstr('CI_ACCOUNT2_PASSWORD'), + ) + self.register_plugins(['xep_0045']) + await self.connect_clients() + + async def test_initial_join(self): + """Check that we can connect to a new muc""" + self.clients[0]['xep_0045'].join_muc(self.muc, 'client1') + presence = await self.clients[0].wait_until('muc::%s::got_online' % self.muc) + self.assertEqual(presence['muc']['affiliation'], 'owner') + + async def test_setup_muc(self): + """Check that sending the initial room config and affiliation list works""" + self.clients[0]['xep_0045'].join_muc(self.muc, 'client1') + presence = await self.clients[0].wait_until('muc::%s::got_online' % self.muc) + self.assertEqual(presence['muc']['affiliation'], 'owner') + # Send initial configuration + config = await self.clients[0]['xep_0045'].get_room_config(self.muc) + values = config.get_values() + values['muc#roomconfig_persistentroom'] = False + values['muc#roomconfig_membersonly'] = True + config['values'] = values + config.reply() + config = await self.clients[0]['xep_0045'].set_room_config(self.muc, config) + + # Send affiliation list including client 2 + await self.clients[0]['xep_0045'].send_affiliation_list( + self.muc, + [ + (self.clients[1].boundjid.bare, 'member'), + ], + ) + + async def test_join_after_config(self): + """Join a room after being added to the affiliation list""" + await self.test_setup_muc() + self.clients[1]['xep_0045'].join_muc(self.muc, 'client2') + await self.clients[1].wait_until('muc::%s::got_online' % self.muc) + + async def test_leave(self): + """Check that we leave properly""" + await self.test_join_after_config() + self.clients[0]['xep_0045'].leave_muc(self.muc, 'client1', 'boooring') + pres = await self.clients[1].wait_until('muc::%s::got_offline' % self.muc) + self.assertEqual(pres['status'], 'boooring') + self.assertEqual(pres['type'], 'unavailable') + + + async def test_kick(self): + """Test kicking a user""" + await self.test_join_after_config() + await asyncio.gather( + self.clients[0].wait_until('muc::%s::got_offline' % self.muc), + self.clients[0]['xep_0045'].set_role(self.muc, 'client2', 'none') + ) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestConnect) From 77587a48432901b5a43441f3e41fe26c02aeb8ed Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 4 Dec 2020 23:08:00 +0100 Subject: [PATCH 5/5] CI: add integration tests to the gitlab pipeline --- .gitlab-ci.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e3daa539..3aa76989 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,6 +13,21 @@ test: - pip3 install emoji aiohttp - ./run_tests.py +test_integration: + stage: test + tags: + - docker + image: ubuntu:latest + only: + variables: + - $CI_ACCOUNT1 + - $CI_ACCOUNT2 + script: + - apt update + - apt install -y python3 python3-pip cython3 gpg + - pip3 install emoji aiohttp aiodns + - ./run_integration_tests.py + trigger_poezio: stage: trigger tags: