Commit 294b4c0a authored by Florian Fuchs's avatar Florian Fuchs

Addedd an improved test harness using WebTest. Contributed by Aurélien Abompard.

parents 8599c9ae 1d333d66
......@@ -30,10 +30,6 @@ from mailmanclient.tests.test_docs import additional_tests
if __name__ == '__main__':
if len(sys.argv) == 1:
print 'Please provide the path to your mailman bin directory.'
os.environ['MAILMAN_TEST_BINDIR'] = sys.argv[1]
suite = additional_tests()
runner = unittest.TextTestRunner(verbosity=2)
......@@ -44,5 +44,5 @@ setup(
# Auto-conversion to Python 3.
install_requires=['httplib2', 'mock', ],
install_requires=['httplib2', 'mock', 'WebTest', ],
......@@ -2,6 +2,12 @@
NEWS for mailman.client
1.0.0b1 (xxxx-xx-xx)
* Addedd an improved test harness using WebTest. Contributed by Aurélien Abompard.
1.0.0a1 (2014-03-15)
* Initial release.
......@@ -640,12 +640,7 @@ Message Moderation
... Some text.
... """
>>> server = smtplib.LMTP('localhost', 8024)
>>> server.sendmail('', '', msg)
>>> server.quit()
(221, 'Bye')
>>> time.sleep(2)
>>> inject_message('', msg)
Messages held for moderation can be listed on a per list basis.
......@@ -17,7 +17,7 @@
"""Test harness for doctests."""
from __future__ import absolute_import, unicode_literals
from __future__ import absolute_import, unicode_literals, print_function
__metaclass__ = type
__all__ = [
......@@ -33,11 +33,16 @@ import doctest
import tempfile
import unittest
import subprocess
from textwrap import dedent
from mock import patch
# pylint: disable-msg=F0401
from pkg_resources import (
resource_filename, resource_exists, resource_listdir, cleanup_resources)
from mailman.config import config
from mailmanclient.tests.utils import FakeMailmanClient, inject_message
DOT = '.'
......@@ -50,17 +55,17 @@ DOCTEST_FLAGS = (
def dump(results):
if results is None:
print None
for key in sorted(results):
if key == 'entries':
for i, entry in enumerate(results[key]):
# entry is a dictionary.
print 'entry %d:' % i
print('entry %d:' % i)
for entry_key in sorted(entry):
print ' {0}: {1}'.format(entry_key, entry[entry_key])
print(' {0}: {1}'.format(entry_key, entry[entry_key]))
print '{0}: {1}'.format(key, results[key])
print('{0}: {1}'.format(key, results[key]))
......@@ -74,69 +79,33 @@ def stop():
def setup(testobj):
"""Test setup."""
# Create a unique database for the running version of Mailman, then start
# it up. It should not yet be running. This environment variable must be
# set to find the installation of Mailman we can run. Yes, this should be
# fixed.
testobj._bindir = os.environ.get('MAILMAN_TEST_BINDIR')
if testobj._bindir is None:
raise RuntimeError('Must set $MAILMAN_TEST_BINDIR to run tests')
vardir = testobj._vardir = tempfile.mkdtemp()
cfgfile = testobj._cfgfile = os.path.join(vardir, 'client_test.cfg')
with open(cfgfile, 'w') as fp:
print >> fp, """\
layout: tmpdir
var_dir: {vardir}
log_dir: /tmp/mmclient/logs
port: 9001
start: no
start: no
start: yes
start: yes
start: yes
start: no
start: yes
start: no
start: no
start: yes
start: no
mailman = os.path.join(testobj._bindir, 'mailman')[mailman, '-C', cfgfile, 'start', '-q'])
# Make sure future statements in our doctests match the Python code. When
# run with 2to3, the future import gets removed and these names are not
# defined.
testobj.globs['absolute_import'] = absolute_import
testobj.globs['unicode_literals'] = unicode_literals
except NameError:
testobj.globs['stop'] = stop
testobj.globs['dump'] = dump
testobj.globs['inject_message'] = inject_message
# In unit tests, passwords aren't encrypted. Don't show this in the doctests
passlib_cfg = os.path.join(config.VAR_DIR, 'passlib.cfg')
with open(passlib_cfg, 'w') as fp:
schemes = sha512_crypt
"""), file=fp)
conf_hash_pw = dedent("""
configuration: {}
config.push('conf_hash_pw', conf_hash_pw)
# Use the FakeMailmanClient
testobj.patcher = patch("mailmanclient.Client", FakeMailmanClient)
fmc = testobj.patcher.start()
def teardown(testobj):
"""Test teardown."""
mailman = os.path.join(testobj._bindir, 'mailman')[mailman, '-C', testobj._cfgfile, 'stop', '-q'])
# Copyright (C) 2010-2014 by The Free Software Foundation, Inc.
# This file is part of mailman.client.
# mailman.client 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.
# mailman.client 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 mailman.client. If not, see <>.
"""Test tools."""
from __future__ import absolute_import, unicode_literals, print_function
import os
from urllib2 import HTTPError
from urlparse import urljoin
from zope.component import getUtility
from webtest import TestApp
from mailman.config import config
from mailman.core.chains import process
from mailman.interfaces.listmanager import IListManager
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import specialized_message_from_string
from mailman.testing.layers import ConfigLayer
import mailmanclient
__metaclass__ = type
__all__ = [
def inject_message(fqdn_listname, msg):
mlist = getUtility(IListManager).get(fqdn_listname)
user_manager = getUtility(IUserManager)
msg = specialized_message_from_string(msg)
for sender in msg.senders:
if user_manager.get_address(sender) is None:
process(mlist, msg, {})
# Mocking mailman client
class FakeConnection(mailmanclient._client._Connection):
Looks for information inside a dict instead of making HTTP requests.
Also, logs the called URLs as called_paths.
Very incomplete at the moment.
def __init__(self, baseurl, name=None, password=None):
super(FakeConnection, self).__init__(baseurl, name, password)
self.called_paths = []
from import make_application = TestApp(make_application())
if self.basic_auth: = ('Basic', (, self.password))
super(FakeConnection, self).__init__(baseurl, name, password)
def call(self, path, data=None, method=None):
{ "path": path, "data": data, "method": method })
if method is None:
if data is None:
method = 'GET'
method = 'POST'
method_fn = getattr(, method.lower())
url = urljoin(self.baseurl, path)
kw = {"expect_errors": True}
if data:
kw["params"] = data
response = method_fn(url, **kw)
headers = response.headers
headers["status"] = response.status_int
content = unicode(response.body)
# If we did not get a 2xx status code, make this look like a
# urllib2 exception, for backward compatibility.
if response.status_int // 100 != 2:
raise HTTPError(url, response.status_int, content, headers, None)
if len(content) == 0:
return headers, None
# XXX Work around for
return headers, response.json
except HTTPError:
except IOError:
raise mailmanclient.MailmanConnectionError(
'Could not connect to Mailman API')
class FakeMailmanClient(mailmanclient.Client):
Subclass of mailmanclient.Client to instantiate a FakeConnection object
instead of the real connection.
def __init__(self, baseurl, name=None, password=None):
self._connection = FakeConnection(baseurl, name, password)
def called_paths(self):
return self._connection.called_paths
def setUp(self):
def tearDown(self):
config.create_paths = False # or ConfigLayer.tearDown will create them
def reset_mailman_config():
# This is necessary because ConfigLayer.setup/tearDown was designed to be
# run only once for the whole test suite, and thus does not reset
# everything afterwards
for prop in ("switchboards", "rules", "chains", "handlers",
"pipelines", "commands"):
getattr(config, prop).clear()
from mailman.model.user import uid_factory
uid_factory._lockobj = None
from mailman.model.member import uid_factory
uid_factory._lockobj = None
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment