~sambuddhabasu1/mailman/fix_mailman_run_error

« back to all changes in this revision

Viewing changes to src/mailman/runners/tests/test_nntp.py

  • Committer: Barry Warsaw
  • Date: 2012-04-01 18:53:38 UTC
  • mfrom: (7139.1.4 bug-967409)
  • Revision ID: barry@list.org-20120401185338-5qujo0c3kc9a8wtr
 * The `news` runner and queue has been renamed to the more accurate `nntp`.
   The runner has also been ported to Mailman 3 (LP: #967409).  Beta testers
   can can safely remove `$var_dir/queue/news`.

 * Configuration schema variable changes:
   [nntp]username -> [nntp]user
   [nntp]port (added)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2012 by the Free Software Foundation, Inc.
 
2
#
 
3
# This file is part of GNU Mailman.
 
4
#
 
5
# GNU Mailman is free software: you can redistribute it and/or modify it under
 
6
# the terms of the GNU General Public License as published by the Free
 
7
# Software Foundation, either version 3 of the License, or (at your option)
 
8
# any later version.
 
9
#
 
10
# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
 
11
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 
12
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 
13
# more details.
 
14
#
 
15
# You should have received a copy of the GNU General Public License along with
 
16
# GNU Mailman.  If not, see <http://www.gnu.org/licenses/>.
 
17
 
 
18
"""Test the NNTP runner and related utilities."""
 
19
 
 
20
from __future__ import absolute_import, print_function, unicode_literals
 
21
 
 
22
__metaclass__ = type
 
23
__all__ = [
 
24
    'TestPrepareMessage',
 
25
    'TestNNTPRunner',
 
26
    ]
 
27
 
 
28
 
 
29
import mock
 
30
import socket
 
31
import nntplib
 
32
import unittest
 
33
 
 
34
from mailman.app.lifecycle import create_list
 
35
from mailman.config import config
 
36
from mailman.interfaces.nntp import NewsModeration
 
37
from mailman.runners import nntp
 
38
from mailman.testing.helpers import (
 
39
    LogFileMark,
 
40
    configuration,
 
41
    get_queue_messages,
 
42
    make_testable_runner,
 
43
    specialized_message_from_string as mfs)
 
44
from mailman.testing.layers import ConfigLayer
 
45
 
 
46
 
 
47
 
 
48
class TestPrepareMessage(unittest.TestCase):
 
49
    """Test message preparation."""
 
50
 
 
51
    layer = ConfigLayer
 
52
 
 
53
    def setUp(self):
 
54
        self._mlist = create_list('test@example.com')
 
55
        self._mlist.linked_newsgroup = 'example.test'
 
56
        self._msg = mfs("""\
 
57
From: anne@example.com
 
58
To: test@example.com
 
59
Subject: A newsgroup posting
 
60
Message-ID: <ant>
 
61
 
 
62
Testing
 
63
""")
 
64
 
 
65
    def test_moderated_approved_header(self):
 
66
        # When the mailing list is moderated , the message will get an
 
67
        # Approved header, which NNTP software uses to forward to the
 
68
        # newsgroup.  The message would not have gotten to the mailing list if
 
69
        # it wasn't already approved.
 
70
        self._mlist.news_moderation = NewsModeration.moderated
 
71
        nntp.prepare_message(self._mlist, self._msg, {})
 
72
        self.assertEqual(self._msg['approved'], 'test@example.com')
 
73
 
 
74
    def test_open_moderated_approved_header(self):
 
75
        # When the mailing list is moderated using an open posting policy, the
 
76
        # message will get an Approved header, which NNTP software uses to
 
77
        # forward to the newsgroup.  The message would not have gotten to the
 
78
        # mailing list if it wasn't already approved.
 
79
        self._mlist.news_moderation = NewsModeration.open_moderated
 
80
        nntp.prepare_message(self._mlist, self._msg, {})
 
81
        self.assertEqual(self._msg['approved'], 'test@example.com')
 
82
 
 
83
    def test_moderation_removes_previous_approved_header(self):
 
84
        # Any existing Approved header is removed from moderated messages.
 
85
        self._msg['Approved'] = 'a bogus approval'
 
86
        self._mlist.news_moderation = NewsModeration.moderated
 
87
        nntp.prepare_message(self._mlist, self._msg, {})
 
88
        headers = self._msg.get_all('approved')
 
89
        self.assertEqual(len(headers), 1)
 
90
        self.assertEqual(headers[0], 'test@example.com')
 
91
 
 
92
    def test_open_moderation_removes_previous_approved_header(self):
 
93
        # Any existing Approved header is removed from moderated messages.
 
94
        self._msg['Approved'] = 'a bogus approval'
 
95
        self._mlist.news_moderation = NewsModeration.open_moderated
 
96
        nntp.prepare_message(self._mlist, self._msg, {})
 
97
        headers = self._msg.get_all('approved')
 
98
        self.assertEqual(len(headers), 1)
 
99
        self.assertEqual(headers[0], 'test@example.com')
 
100
 
 
101
    def test_stripped_subject(self):
 
102
        # The cook-headers handler adds the original and/or stripped (of the
 
103
        # prefix) subject to the metadata.  Assume that handler's been run;
 
104
        # check the Subject header.
 
105
        self._mlist.news_prefix_subject_too = False
 
106
        del self._msg['subject']
 
107
        self._msg['subject'] = 'Re: Your test'
 
108
        msgdata = dict(stripped_subject='Your test')
 
109
        nntp.prepare_message(self._mlist, self._msg, msgdata)
 
110
        headers = self._msg.get_all('subject')
 
111
        self.assertEqual(len(headers), 1)
 
112
        self.assertEqual(headers[0], 'Your test')
 
113
 
 
114
    def test_original_subject(self):
 
115
        # The cook-headers handler adds the original and/or stripped (of the
 
116
        # prefix) subject to the metadata.  Assume that handler's been run;
 
117
        # check the Subject header.
 
118
        self._mlist.news_prefix_subject_too = False
 
119
        del self._msg['subject']
 
120
        self._msg['subject'] = 'Re: Your test'
 
121
        msgdata = dict(original_subject='Your test')
 
122
        nntp.prepare_message(self._mlist, self._msg, msgdata)
 
123
        headers = self._msg.get_all('subject')
 
124
        self.assertEqual(len(headers), 1)
 
125
        self.assertEqual(headers[0], 'Your test')
 
126
 
 
127
    def test_stripped_subject_prefix_okay(self):
 
128
        # The cook-headers handler adds the original and/or stripped (of the
 
129
        # prefix) subject to the metadata.  Assume that handler's been run;
 
130
        # check the Subject header.
 
131
        self._mlist.news_prefix_subject_too = True
 
132
        del self._msg['subject']
 
133
        self._msg['subject'] = 'Re: Your test'
 
134
        msgdata = dict(stripped_subject='Your test')
 
135
        nntp.prepare_message(self._mlist, self._msg, msgdata)
 
136
        headers = self._msg.get_all('subject')
 
137
        self.assertEqual(len(headers), 1)
 
138
        self.assertEqual(headers[0], 'Re: Your test')
 
139
 
 
140
    def test_original_subject_prefix_okay(self):
 
141
        # The cook-headers handler adds the original and/or stripped (of the
 
142
        # prefix) subject to the metadata.  Assume that handler's been run;
 
143
        # check the Subject header.
 
144
        self._mlist.news_prefix_subject_too = True
 
145
        del self._msg['subject']
 
146
        self._msg['subject'] = 'Re: Your test'
 
147
        msgdata = dict(original_subject='Your test')
 
148
        nntp.prepare_message(self._mlist, self._msg, msgdata)
 
149
        headers = self._msg.get_all('subject')
 
150
        self.assertEqual(len(headers), 1)
 
151
        self.assertEqual(headers[0], 'Re: Your test')
 
152
 
 
153
    def test_add_newsgroups_header(self):
 
154
        # Prepared messages get a Newsgroups header.
 
155
        msgdata = dict(original_subject='Your test')
 
156
        nntp.prepare_message(self._mlist, self._msg, msgdata)
 
157
        self.assertEqual(self._msg['newsgroups'], 'example.test')
 
158
 
 
159
    def test_add_newsgroups_header_to_existing(self):
 
160
        # If the message already has a Newsgroups header, the linked newsgroup
 
161
        # gets appended to that value, using comma-space separated lists.
 
162
        self._msg['Newsgroups'] = 'foo.test, bar.test'
 
163
        msgdata = dict(original_subject='Your test')
 
164
        nntp.prepare_message(self._mlist, self._msg, msgdata)
 
165
        headers = self._msg.get_all('newsgroups')
 
166
        self.assertEqual(len(headers), 1)
 
167
        self.assertEqual(headers[0], 'foo.test, bar.test, example.test')
 
168
 
 
169
    def test_add_lines_header(self):
 
170
        # A Lines: header seems useful.
 
171
        nntp.prepare_message(self._mlist, self._msg, {})
 
172
        self.assertEqual(self._msg['lines'], '1')
 
173
 
 
174
    def test_the_message_has_been_prepared(self):
 
175
        # A key gets added to the metadata so that a retry won't try to
 
176
        # re-apply all the preparations.
 
177
        msgdata = {}
 
178
        nntp.prepare_message(self._mlist, self._msg, msgdata)
 
179
        self.assertTrue(msgdata.get('prepped'))
 
180
 
 
181
    @configuration('nntp', remove_headers='x-complaints-to')
 
182
    def test_remove_headers(self):
 
183
        # During preparation, headers which cause problems with certain NNTP
 
184
        # servers such as INN get removed.
 
185
        self._msg['X-Complaints-To'] = 'arguments@example.com'
 
186
        nntp.prepare_message(self._mlist, self._msg, {})
 
187
        self.assertEqual(self._msg['x-complaints-to'], None)
 
188
 
 
189
    @configuration('nntp', rewrite_duplicate_headers="""
 
190
        To X-Original-To
 
191
        X-Fake X-Original-Fake
 
192
        """)
 
193
    def test_rewrite_headers(self):
 
194
        # Some NNTP servers are very strict about duplicate headers.  What we
 
195
        # can do is look at some headers and if they is more than one of that
 
196
        # header in the message, all the headers are deleted except the first
 
197
        # one, and then the other values are moved to the destination header.
 
198
        #
 
199
        # In this example, we'll create multiple To headers, which will all
 
200
        # get moved to X-Original-To.  However, because there will only be one
 
201
        # X-Fake header, it doesn't get rewritten.
 
202
        self._msg['To'] = 'test@example.org'
 
203
        self._msg['To'] = 'test@example.net'
 
204
        self._msg['X-Fake'] = 'ignore me'
 
205
        self.assertEqual(len(self._msg.get_all('to')), 3)
 
206
        self.assertEqual(len(self._msg.get_all('x-fake')), 1)
 
207
        nntp.prepare_message(self._mlist, self._msg, {})
 
208
        tos = self._msg.get_all('to')
 
209
        self.assertEqual(len(tos), 1)
 
210
        self.assertEqual(tos[0], 'test@example.com')
 
211
        original_tos = self._msg.get_all('x-original-to')
 
212
        self.assertEqual(len(original_tos), 2)
 
213
        self.assertEqual(original_tos,
 
214
                         ['test@example.org', 'test@example.net'])
 
215
        fakes = self._msg.get_all('x-fake')
 
216
        self.assertEqual(len(fakes), 1)
 
217
        self.assertEqual(fakes[0], 'ignore me')
 
218
        self.assertEqual(self._msg.get_all('x-original-fake'), None)
 
219
 
 
220
    @configuration('nntp', rewrite_duplicate_headers="""
 
221
        To X-Original-To
 
222
        X-Fake
 
223
        """)
 
224
    def test_odd_duplicates(self):
 
225
        # This is just a corner case, where there is an odd number of rewrite
 
226
        # headers.  In that case, the odd-one-out does not get rewritten.
 
227
        self._msg['x-fake'] = 'one'
 
228
        self._msg['x-fake'] = 'two'
 
229
        self._msg['x-fake'] = 'three'
 
230
        self.assertEqual(len(self._msg.get_all('x-fake')), 3)
 
231
        nntp.prepare_message(self._mlist, self._msg, {})
 
232
        fakes = self._msg.get_all('x-fake')
 
233
        self.assertEqual(len(fakes), 3)
 
234
        self.assertEqual(fakes, ['one', 'two', 'three'])
 
235
 
 
236
 
 
237
 
 
238
class TestNNTPRunner(unittest.TestCase):
 
239
    """The NNTP runner hands messages off to the NNTP server."""
 
240
 
 
241
    layer = ConfigLayer
 
242
 
 
243
    def setUp(self):
 
244
        self._mlist = create_list('test@example.com')
 
245
        self._mlist.linked_newsgroup = 'example.test'
 
246
        self._msg = mfs("""\
 
247
From: anne@example.com
 
248
To: test@example.com
 
249
Subject: A newsgroup posting
 
250
Message-ID: <ant>
 
251
 
 
252
Testing
 
253
""")
 
254
        self._runner = make_testable_runner(nntp.NNTPRunner, 'nntp')
 
255
        self._nntpq = config.switchboards['nntp']
 
256
 
 
257
    @mock.patch('nntplib.NNTP')
 
258
    def test_connect(self, class_mock):
 
259
        # Test connection to the NNTP server with default values.
 
260
        self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
 
261
        self._runner.run()
 
262
        class_mock.assert_called_once_with(
 
263
            '', 119, user='', password='', readermode=True)
 
264
 
 
265
    @configuration('nntp', user='alpha', password='beta',
 
266
                   host='nntp.example.com', port='2112')
 
267
    @mock.patch('nntplib.NNTP')
 
268
    def test_connect_with_configuration(self, class_mock):
 
269
        # Test connection to the NNTP server with specific values.
 
270
        self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
 
271
        self._runner.run()
 
272
        class_mock.assert_called_once_with(
 
273
            'nntp.example.com', 2112,
 
274
            user='alpha', password='beta', readermode=True)
 
275
 
 
276
    @mock.patch('nntplib.NNTP')
 
277
    def test_post(self, class_mock):
 
278
        # Test that the message is posted to the NNTP server.
 
279
        self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
 
280
        self._runner.run()
 
281
        # Get the mocked instance, which was used in the runner.
 
282
        conn_mock = class_mock()
 
283
        # The connection object's post() method was called once with a
 
284
        # file-like object containing the message's bytes.  Read those bytes
 
285
        # and make some simple checks that the message is what we expected.
 
286
        args = conn_mock.post.call_args
 
287
        # One positional argument.
 
288
        self.assertEqual(len(args[0]), 1)
 
289
        # No keyword arguments.
 
290
        self.assertEqual(len(args[1]), 0)
 
291
        msg = mfs(args[0][0].read())
 
292
        self.assertEqual(msg['subject'], 'A newsgroup posting')
 
293
 
 
294
    @mock.patch('nntplib.NNTP')
 
295
    def test_connection_got_quit(self, class_mock):
 
296
        # The NNTP connection gets closed after a successful post.
 
297
        # Test that the message is posted to the NNTP server.
 
298
        self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
 
299
        self._runner.run()
 
300
        # Get the mocked instance, which was used in the runner.
 
301
        conn_mock = class_mock()
 
302
        # The connection object's post() method was called once with a
 
303
        # file-like object containing the message's bytes.  Read those bytes
 
304
        # and make some simple checks that the message is what we expected.
 
305
        conn_mock.quit.assert_called_once_with()
 
306
 
 
307
    @mock.patch('nntplib.NNTP', side_effect=nntplib.error_temp)
 
308
    def test_connect_with_nntplib_failure(self, class_mock):
 
309
        self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
 
310
        mark = LogFileMark('mailman.error')
 
311
        self._runner.run()
 
312
        log_message = mark.readline()[:-1]
 
313
        self.assertTrue(log_message.endswith(
 
314
            'NNTP error for test@example.com'))
 
315
 
 
316
    @mock.patch('nntplib.NNTP', side_effect=socket.error)
 
317
    def test_connect_with_socket_failure(self, class_mock):
 
318
        self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
 
319
        mark = LogFileMark('mailman.error')
 
320
        self._runner.run()
 
321
        log_message = mark.readline()[:-1]
 
322
        self.assertTrue(log_message.endswith(
 
323
            'NNTP socket error for test@example.com'))
 
324
 
 
325
    @mock.patch('nntplib.NNTP', side_effect=RuntimeError)
 
326
    def test_connect_with_other_failure(self, class_mock):
 
327
        # In this failure mode, the message stays queued, so we can only run
 
328
        # the nntp runner once.
 
329
        def once(runner):
 
330
            # I.e. stop immediately, since the queue will not be empty.
 
331
            return True
 
332
        runner = make_testable_runner(nntp.NNTPRunner, 'nntp', predicate=once)
 
333
        self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
 
334
        mark = LogFileMark('mailman.error')
 
335
        runner.run()
 
336
        log_message = mark.readline()[:-1]
 
337
        self.assertTrue(log_message.endswith(
 
338
            'NNTP unexpected exception for test@example.com'))
 
339
        messages = get_queue_messages('nntp')
 
340
        self.assertEqual(len(messages), 1)
 
341
        self.assertEqual(messages[0].msgdata['listname'], 'test@example.com')
 
342
        self.assertEqual(messages[0].msg['subject'], 'A newsgroup posting')
 
343
 
 
344
    @mock.patch('nntplib.NNTP', side_effect=nntplib.error_temp)
 
345
    def test_connection_never_gets_quit_after_failures(self, class_mock):
 
346
        # The NNTP connection doesn't get closed after a unsuccessful
 
347
        # connection, since there's nothing to close.
 
348
        self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
 
349
        self._runner.run()
 
350
        # Get the mocked instance, which was used in the runner.  Turn off the
 
351
        # exception raising side effect first though!
 
352
        class_mock.side_effect = None
 
353
        conn_mock = class_mock()
 
354
        # The connection object's post() method was called once with a
 
355
        # file-like object containing the message's bytes.  Read those bytes
 
356
        # and make some simple checks that the message is what we expected.
 
357
        self.assertEqual(conn_mock.quit.call_count, 0)
 
358
 
 
359
    @mock.patch('nntplib.NNTP')
 
360
    def test_connection_got_quit_after_post_failure(self, class_mock):
 
361
        # The NNTP connection does get closed after a unsuccessful post.
 
362
        # Add a side-effect to the instance mock's .post() method.
 
363
        conn_mock = class_mock()
 
364
        conn_mock.post.side_effect = nntplib.error_temp
 
365
        self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
 
366
        self._runner.run()
 
367
        # The connection object's post() method was called once with a
 
368
        # file-like object containing the message's bytes.  Read those bytes
 
369
        # and make some simple checks that the message is what we expected.
 
370
        conn_mock.quit.assert_called_once_with()