Merge lp:~abompard/mailman.client/mockingclient into lp:mailman.client

Proposed by Aurélien Bompard
Status: Merged
Merged at revision: 58
Proposed branch: lp:~abompard/mailman.client/mockingclient
Merge into: lp:mailman.client
Diff against target: 322 lines (+179/-71)
4 files modified
setup.py (+1/-1)
src/mailmanclient/docs/using.txt (+1/-6)
src/mailmanclient/tests/test_docs.py (+33/-64)
src/mailmanclient/tests/utils.py (+144/-0)
To merge this branch: bzr merge lp:~abompard/mailman.client/mockingclient
Reviewer Review Type Date Requested Status
Mailman Coders Pending
Review via email: mp+242185@code.launchpad.net

Description of the change

This branch adds a mocking client which only instantiates the REST API server in memory, not by forking a specific mailman server.
Then, the requests are made through the WSGI API using WebTest.

To post a comment you must log in.
61. By Aurélien Bompard

Make it possible to run the setup/teardown methods more than once per test run

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'setup.py'
2--- setup.py 2014-04-22 14:38:40 +0000
3+++ setup.py 2014-11-20 09:25:46 +0000
4@@ -44,5 +44,5 @@
5 # Auto-conversion to Python 3.
6 use_2to3=True,
7 convert_2to3_doctests=find_doctests(),
8- install_requires=['httplib2', 'mock', ],
9+ install_requires=['httplib2', 'mock', 'WebTest', ],
10 )
11
12=== modified file 'src/mailmanclient/docs/using.txt'
13--- src/mailmanclient/docs/using.txt 2014-04-22 12:47:00 +0000
14+++ src/mailmanclient/docs/using.txt 2014-11-20 09:25:46 +0000
15@@ -640,12 +640,7 @@
16 ... Some text.
17 ...
18 ... """
19- >>> server = smtplib.LMTP('localhost', 8024)
20- >>> server.sendmail('nomember@example.com', 'test-one@example.com', msg)
21- {}
22- >>> server.quit()
23- (221, 'Bye')
24- >>> time.sleep(2)
25+ >>> inject_message('test-one@example.com', msg)
26
27 Messages held for moderation can be listed on a per list basis.
28
29
30=== modified file 'src/mailmanclient/tests/test_docs.py'
31--- src/mailmanclient/tests/test_docs.py 2014-03-15 20:43:52 +0000
32+++ src/mailmanclient/tests/test_docs.py 2014-11-20 09:25:46 +0000
33@@ -17,7 +17,7 @@
34
35 """Test harness for doctests."""
36
37-from __future__ import absolute_import, unicode_literals
38+from __future__ import absolute_import, unicode_literals, print_function
39
40 __metaclass__ = type
41 __all__ = [
42@@ -33,11 +33,16 @@
43 import tempfile
44 import unittest
45 import subprocess
46+from textwrap import dedent
47
48+from mock import patch
49 # pylint: disable-msg=F0401
50 from pkg_resources import (
51 resource_filename, resource_exists, resource_listdir, cleanup_resources)
52
53+from mailman.config import config
54+from mailmanclient.tests.utils import FakeMailmanClient, inject_message
55+
56
57 COMMASPACE = ', '
58 DOT = '.'
59@@ -50,17 +55,17 @@
60
61
62 def dump(results):
63 if results is None:
64- print None
65+ print(None)
66 return
67 for key in sorted(results):
68 if key == 'entries':
69 for i, entry in enumerate(results[key]):
70 # entry is a dictionary.
71- print 'entry %d:' % i
72+ print('entry %d:' % i)
73 for entry_key in sorted(entry):
74- print ' {0}: {1}'.format(entry_key, entry[entry_key])
75+ print(' {0}: {1}'.format(entry_key, entry[entry_key]))
76 else:
77- print '{0}: {1}'.format(key, results[key])
78+ print('{0}: {1}'.format(key, results[key]))
79
80
81
82
83@@ -74,69 +79,33 @@
84
85
86
87 def setup(testobj):
88- """Test setup."""
89- # Create a unique database for the running version of Mailman, then start
90- # it up. It should not yet be running. This environment variable must be
91- # set to find the installation of Mailman we can run. Yes, this should be
92- # fixed.
93- testobj._bindir = os.environ.get('MAILMAN_TEST_BINDIR')
94- if testobj._bindir is None:
95- raise RuntimeError('Must set $MAILMAN_TEST_BINDIR to run tests')
96- vardir = testobj._vardir = tempfile.mkdtemp()
97- cfgfile = testobj._cfgfile = os.path.join(vardir, 'client_test.cfg')
98- with open(cfgfile, 'w') as fp:
99- print >> fp, """\
100-[mailman]
101-layout: tmpdir
102-[paths.tmpdir]
103-var_dir: {vardir}
104-log_dir: /tmp/mmclient/logs
105-[webservice]
106-port: 9001
107-[runner.archive]
108-start: no
109-[runner.bounces]
110-start: no
111-[runner.command]
112-start: yes
113-[runner.in]
114-start: yes
115-[runner.lmtp]
116-start: yes
117-[runner.news]
118-start: no
119-[runner.out]
120-start: yes
121-[runner.pipeline]
122-start: no
123-[runner.retry]
124-start: no
125-[runner.virgin]
126-start: yes
127-[runner.digest]
128-start: no
129-""".format(vardir=vardir)
130- mailman = os.path.join(testobj._bindir, 'mailman')
131- subprocess.call([mailman, '-C', cfgfile, 'start', '-q'])
132- time.sleep(3)
133- # Make sure future statements in our doctests match the Python code. When
134- # run with 2to3, the future import gets removed and these names are not
135- # defined.
136- try:
137- testobj.globs['absolute_import'] = absolute_import
138- testobj.globs['unicode_literals'] = unicode_literals
139- except NameError:
140- pass
141 testobj.globs['stop'] = stop
142 testobj.globs['dump'] = dump
143-
144-
145+ testobj.globs['inject_message'] = inject_message
146+ FakeMailmanClient.setUp()
147+ # In unit tests, passwords aren't encrypted. Don't show this in the doctests
148+ passlib_cfg = os.path.join(config.VAR_DIR, 'passlib.cfg')
149+ with open(passlib_cfg, 'w') as fp:
150+ print(dedent("""
151+ [passlib]
152+ schemes = sha512_crypt
153+ """), file=fp)
154+ conf_hash_pw = dedent("""
155+ [passwords]
156+ configuration: {}
157+ """.format(passlib_cfg))
158+ config.push('conf_hash_pw', conf_hash_pw)
159+ # Use the FakeMailmanClient
160+ testobj.patcher = patch("mailmanclient.Client", FakeMailmanClient)
161+ fmc = testobj.patcher.start()
162+
163+
164+
165
166 def teardown(testobj):
167 """Test teardown."""
168- mailman = os.path.join(testobj._bindir, 'mailman')
169- subprocess.call([mailman, '-C', testobj._cfgfile, 'stop', '-q'])
170- shutil.rmtree(testobj._vardir)
171- time.sleep(3)
172+ testobj.patcher.stop()
173+ config.pop('conf_hash_pw')
174+ FakeMailmanClient.tearDown()
175
176
177
178
179
180=== added file 'src/mailmanclient/tests/utils.py'
181--- src/mailmanclient/tests/utils.py 1970-01-01 00:00:00 +0000
182+++ src/mailmanclient/tests/utils.py 2014-11-20 09:25:46 +0000
183@@ -0,0 +1,144 @@
184+# Copyright (C) 2010-2014 by The Free Software Foundation, Inc.
185+#
186+# This file is part of mailman.client.
187+#
188+# mailman.client is free software: you can redistribute it and/or modify it
189+# under the terms of the GNU Lesser General Public License as published by the
190+# Free Software Foundation, either version 3 of the License, or (at your
191+# option) any later version.
192+#
193+# mailman.client is distributed in the hope that it will be useful, but WITHOUT
194+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
195+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
196+# for more details.
197+#
198+# You should have received a copy of the GNU Lesser General Public License
199+# along with mailman.client. If not, see <http://www.gnu.org/licenses/>.
200+
201+"""Test tools."""
202+
203+from __future__ import absolute_import, unicode_literals, print_function
204+
205+import os
206+from urllib2 import HTTPError
207+from urlparse import urljoin
208+
209+from zope.component import getUtility
210+from webtest import TestApp
211+
212+from mailman.config import config
213+from mailman.core.chains import process
214+from mailman.interfaces.listmanager import IListManager
215+from mailman.interfaces.usermanager import IUserManager
216+from mailman.testing.helpers import specialized_message_from_string
217+from mailman.testing.layers import ConfigLayer
218+import mailmanclient
219+
220+__metaclass__ = type
221+__all__ = [
222+ "inject_message",
223+ "FakeMailmanClient",
224+ ]
225+
226+
227+
228+def inject_message(fqdn_listname, msg):
229+ mlist = getUtility(IListManager).get(fqdn_listname)
230+ user_manager = getUtility(IUserManager)
231+ msg = specialized_message_from_string(msg)
232+ for sender in msg.senders:
233+ if user_manager.get_address(sender) is None:
234+ user_manager.create_address(sender)
235+ process(mlist, msg, {})
236+
237+
238+#
239+# Mocking mailman client
240+#
241+
242+
243+class FakeConnection(mailmanclient._client._Connection):
244+ """
245+ Looks for information inside a dict instead of making HTTP requests.
246+ Also, logs the called URLs as called_paths.
247+ Very incomplete at the moment.
248+ """
249+
250+ def __init__(self, baseurl, name=None, password=None):
251+ super(FakeConnection, self).__init__(baseurl, name, password)
252+ self.called_paths = []
253+ from mailman.rest.wsgiapp import make_application
254+ self.app = TestApp(make_application())
255+ if self.basic_auth:
256+ self.app.authorization = ('Basic', (self.name, self.password))
257+ super(FakeConnection, self).__init__(baseurl, name, password)
258+
259+ def call(self, path, data=None, method=None):
260+ self.called_paths.append(
261+ { "path": path, "data": data, "method": method })
262+ if method is None:
263+ if data is None:
264+ method = 'GET'
265+ else:
266+ method = 'POST'
267+ method_fn = getattr(self.app, method.lower())
268+ url = urljoin(self.baseurl, path)
269+ try:
270+ kw = {"expect_errors": True}
271+ if data:
272+ kw["params"] = data
273+ response = method_fn(url, **kw)
274+ headers = response.headers
275+ headers["status"] = response.status_int
276+ content = unicode(response.body)
277+ # If we did not get a 2xx status code, make this look like a
278+ # urllib2 exception, for backward compatibility.
279+ if response.status_int // 100 != 2:
280+ raise HTTPError(url, response.status_int, content, headers, None)
281+ if len(content) == 0:
282+ return headers, None
283+ # XXX Work around for http://bugs.python.org/issue10038
284+ return headers, response.json
285+ except HTTPError:
286+ raise
287+ except IOError:
288+ raise mailmanclient.MailmanConnectionError(
289+ 'Could not connect to Mailman API')
290+
291+
292+
293+class FakeMailmanClient(mailmanclient.Client):
294+ """
295+ Subclass of mailmanclient.Client to instantiate a FakeConnection object
296+ instead of the real connection.
297+ """
298+
299+ def __init__(self, baseurl, name=None, password=None):
300+ self._connection = FakeConnection(baseurl, name, password)
301+
302+ @property
303+ def called_paths(self):
304+ return self._connection.called_paths
305+
306+ @classmethod
307+ def setUp(self):
308+ ConfigLayer.setUp()
309+
310+ @classmethod
311+ def tearDown(self):
312+ config.create_paths = False # or ConfigLayer.tearDown will create them
313+ ConfigLayer.testTearDown()
314+ ConfigLayer.tearDown()
315+ reset_mailman_config()
316+
317+def reset_mailman_config():
318+ # This is necessary because ConfigLayer.setup/tearDown was designed to be
319+ # run only once for the whole test suite, and thus does not reset
320+ # everything afterwards
321+ for prop in ("switchboards", "rules", "chains", "handlers",
322+ "pipelines", "commands"):
323+ getattr(config, prop).clear()
324+ from mailman.model.user import uid_factory
325+ uid_factory._lockobj = None
326+ from mailman.model.member import uid_factory
327+ uid_factory._lockobj = None

Subscribers

People subscribed via source and target branches