1 # Copyright (C) 2011-2015 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
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)
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
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/>.
18 """Tests for the subscription service."""
22 'TestSubscriptionWorkflow',
29 from mailman
.app
.lifecycle
import create_list
30 from mailman
.app
.subscriptions
import SubscriptionWorkflow
31 from mailman
.interfaces
.address
import InvalidEmailAddressError
32 from mailman
.interfaces
.bans
import IBanManager
33 from mailman
.interfaces
.member
import (
34 MemberRole
, MembershipIsBannedError
, MissingPreferredAddressError
)
35 from mailman
.interfaces
.pending
import IPendings
36 from mailman
.interfaces
.subscriptions
import (
37 MissingUserError
, ISubscriptionService
)
38 from mailman
.testing
.helpers
import LogFileMark
, get_queue_messages
39 from mailman
.testing
.layers
import ConfigLayer
40 from mailman
.interfaces
.mailinglist
import SubscriptionPolicy
41 from mailman
.interfaces
.usermanager
import IUserManager
42 from mailman
.interfaces
.workflow
import IWorkflowStateManager
43 from mailman
.utilities
.datetime
import now
44 from unittest
.mock
import patch
45 from zope
.component
import getUtility
49 class TestJoin(unittest
.TestCase
):
53 self
._mlist
= create_list('test@example.com')
54 self
._service
= getUtility(ISubscriptionService
)
56 def test_join_user_with_bogus_id(self
):
57 # When `subscriber` is a missing user id, an exception is raised.
58 with self
.assertRaises(MissingUserError
) as cm
:
59 self
._service
.join('test.example.com', uuid
.UUID(int=99))
60 self
.assertEqual(cm
.exception
.user_id
, uuid
.UUID(int=99))
62 def test_join_user_with_invalid_email_address(self
):
63 # When `subscriber` is a string that is not an email address, an
64 # exception is raised.
65 with self
.assertRaises(InvalidEmailAddressError
) as cm
:
66 self
._service
.join('test.example.com', 'bogus')
67 self
.assertEqual(cm
.exception
.email
, 'bogus')
69 def test_missing_preferred_address(self
):
70 # A user cannot join a mailing list if they have no preferred address.
71 anne
= self
._service
.join(
72 'test.example.com', 'anne@example.com', 'Anne Person')
73 # Try to join Anne as a user with a different role. Her user has no
74 # preferred address, so this will fail.
75 self
.assertRaises(MissingPreferredAddressError
,
77 'test.example.com', anne
.user
.user_id
,
78 role
=MemberRole
.owner
)
82 class TestSubscriptionWorkflow(unittest
.TestCase
):
87 self
._mlist
= create_list('test@example.com')
88 self
._mlist
.admin_immed_notify
= False
89 self
._anne
= 'anne@example.com'
90 self
._user
_manager
= getUtility(IUserManager
)
92 def test_user_or_address_required(self
):
93 # The `subscriber` attribute must be a user or address.
94 workflow
= SubscriptionWorkflow(self
._mlist
)
95 self
.assertRaises(AssertionError, list, workflow
)
97 def test_sanity_checks_address(self
):
98 # Ensure that the sanity check phase, when given an IAddress, ends up
100 anne
= self
._user
_manager
.create_address(self
._anne
)
101 workflow
= SubscriptionWorkflow(self
._mlist
, anne
)
102 self
.assertIsNotNone(workflow
.address
)
103 self
.assertIsNone(workflow
.user
)
104 workflow
.run_thru('sanity_checks')
105 self
.assertIsNotNone(workflow
.address
)
106 self
.assertIsNotNone(workflow
.user
)
107 self
.assertEqual(list(workflow
.user
.addresses
)[0].email
, self
._anne
)
109 def test_sanity_checks_user_with_preferred_address(self
):
110 # Ensure that the sanity check phase, when given an IUser with a
111 # preferred address, ends up with an address.
112 anne
= self
._user
_manager
.make_user(self
._anne
)
113 address
= list(anne
.addresses
)[0]
114 address
.verified_on
= now()
115 anne
.preferred_address
= address
116 workflow
= SubscriptionWorkflow(self
._mlist
, anne
)
117 # The constructor sets workflow.address because the user has a
119 self
.assertEqual(workflow
.address
, address
)
120 self
.assertEqual(workflow
.user
, anne
)
121 workflow
.run_thru('sanity_checks')
122 self
.assertEqual(workflow
.address
, address
)
123 self
.assertEqual(workflow
.user
, anne
)
125 def test_sanity_checks_user_without_preferred_address(self
):
126 # Ensure that the sanity check phase, when given a user without a
127 # preferred address, but with at least one linked address, gets an
129 anne
= self
._user
_manager
.make_user(self
._anne
)
130 workflow
= SubscriptionWorkflow(self
._mlist
, anne
)
131 self
.assertIsNone(workflow
.address
)
132 self
.assertEqual(workflow
.user
, anne
)
133 workflow
.run_thru('sanity_checks')
134 self
.assertIsNotNone(workflow
.address
)
135 self
.assertEqual(workflow
.user
, anne
)
137 def test_sanity_checks_user_with_multiple_linked_addresses(self
):
138 # Ensure that the santiy check phase, when given a user without a
139 # preferred address, but with multiple linked addresses, gets of of
140 # those addresses (exactly which one is undefined).
141 anne
= self
._user
_manager
.make_user(self
._anne
)
142 anne
.link(self
._user
_manager
.create_address('anne@example.net'))
143 anne
.link(self
._user
_manager
.create_address('anne@example.org'))
144 workflow
= SubscriptionWorkflow(self
._mlist
, anne
)
145 self
.assertIsNone(workflow
.address
)
146 self
.assertEqual(workflow
.user
, anne
)
147 workflow
.run_thru('sanity_checks')
148 self
.assertIn(workflow
.address
.email
, ['anne@example.com',
151 self
.assertEqual(workflow
.user
, anne
)
153 def test_sanity_checks_user_without_addresses(self
):
154 # It is an error to try to subscribe a user with no linked addresses.
155 user
= self
._user
_manager
.create_user()
156 workflow
= SubscriptionWorkflow(self
._mlist
, user
)
157 self
.assertRaises(AssertionError, workflow
.run_thru
, 'sanity_checks')
159 def test_sanity_checks_globally_banned_address(self
):
160 # An exception is raised if the address is globally banned.
161 anne
= self
._user
_manager
.create_address(self
._anne
)
162 IBanManager(None).ban(self
._anne
)
163 workflow
= SubscriptionWorkflow(self
._mlist
, anne
)
164 self
.assertRaises(MembershipIsBannedError
, list, workflow
)
166 def test_sanity_checks_banned_address(self
):
167 # An exception is raised if the address is banned by the mailing list.
168 anne
= self
._user
_manager
.create_address(self
._anne
)
169 IBanManager(self
._mlist
).ban(self
._anne
)
170 workflow
= SubscriptionWorkflow(self
._mlist
, anne
)
171 self
.assertRaises(MembershipIsBannedError
, list, workflow
)
173 def test_verification_checks_with_verified_address(self
):
174 # When the address is already verified, we skip straight to the
175 # confirmation checks.
176 anne
= self
._user
_manager
.create_address(self
._anne
)
177 anne
.verified_on
= now()
178 workflow
= SubscriptionWorkflow(self
._mlist
, anne
)
179 workflow
.run_thru('verification_checks')
180 with patch
.object(workflow
, '_step_confirmation_checks') as step
:
182 step
.assert_called_once_with()
184 def test_verification_checks_with_pre_verified_address(self
):
185 # When the address is not yet verified, but the pre-verified flag is
186 # passed to the workflow, we skip to the confirmation checks.
187 anne
= self
._user
_manager
.create_address(self
._anne
)
188 workflow
= SubscriptionWorkflow(self
._mlist
, anne
, pre_verified
=True)
189 workflow
.run_thru('verification_checks')
190 with patch
.object(workflow
, '_step_confirmation_checks') as step
:
192 step
.assert_called_once_with()
193 # And now the address is verified.
194 self
.assertIsNotNone(anne
.verified_on
)
196 def test_verification_checks_confirmation_needed(self
):
197 # The address is neither verified, nor is the pre-verified flag set.
198 # A confirmation message must be sent to the user which will also
199 # verify their address.
200 anne
= self
._user
_manager
.create_address(self
._anne
)
201 workflow
= SubscriptionWorkflow(self
._mlist
, anne
)
202 workflow
.run_thru('verification_checks')
203 with patch
.object(workflow
, '_step_send_confirmation') as step
:
205 step
.assert_called_once_with()
206 # The address still hasn't been verified.
207 self
.assertIsNone(anne
.verified_on
)
209 def test_confirmation_checks_open_list(self
):
210 # A subscription to an open list does not need to be confirmed or
212 self
._mlist
.subscription_policy
= SubscriptionPolicy
.open
213 anne
= self
._user
_manager
.create_address(self
._anne
)
214 workflow
= SubscriptionWorkflow(self
._mlist
, anne
, pre_verified
=True)
215 workflow
.run_thru('confirmation_checks')
216 with patch
.object(workflow
, '_step_do_subscription') as step
:
218 step
.assert_called_once_with()
220 def test_confirmation_checks_no_user_confirmation_needed(self
):
221 # A subscription to a list which does not need user confirmation skips
222 # to the moderation checks.
223 self
._mlist
.subscription_policy
= SubscriptionPolicy
.moderate
224 anne
= self
._user
_manager
.create_address(self
._anne
)
225 workflow
= SubscriptionWorkflow(self
._mlist
, anne
, pre_verified
=True)
226 workflow
.run_thru('confirmation_checks')
227 with patch
.object(workflow
, '_step_moderation_checks') as step
:
229 step
.assert_called_once_with()
231 def test_confirmation_checks_confirm_pre_confirmed(self
):
232 # The subscription policy requires user confirmation, but their
233 # subscription is pre-confirmed.
234 self
._mlist
.subscription_policy
= SubscriptionPolicy
.confirm
235 anne
= self
._user
_manager
.create_address(self
._anne
)
236 workflow
= SubscriptionWorkflow(self
._mlist
, anne
,
239 workflow
.run_thru('confirmation_checks')
240 with patch
.object(workflow
, '_step_moderation_checks') as step
:
242 step
.assert_called_once_with()
244 def test_confirmation_checks_confirm_and_moderate_pre_confirmed(self
):
245 # The subscription policy requires user confirmation and moderation,
246 # but their subscription is pre-confirmed.
247 self
._mlist
.subscription_policy
= \
248 SubscriptionPolicy
.confirm_then_moderate
249 anne
= self
._user
_manager
.create_address(self
._anne
)
250 workflow
= SubscriptionWorkflow(self
._mlist
, anne
,
253 workflow
.run_thru('confirmation_checks')
254 with patch
.object(workflow
, '_step_moderation_checks') as step
:
256 step
.assert_called_once_with()
258 def test_confirmation_checks_confirmation_needed(self
):
259 # The subscription policy requires confirmation and the subscription
260 # is not pre-confirmed.
261 self
._mlist
.subscription_policy
= SubscriptionPolicy
.confirm
262 anne
= self
._user
_manager
.create_address(self
._anne
)
263 workflow
= SubscriptionWorkflow(self
._mlist
, anne
, pre_verified
=True)
264 workflow
.run_thru('confirmation_checks')
265 with patch
.object(workflow
, '_step_send_confirmation') as step
:
267 step
.assert_called_once_with()
269 def test_confirmation_checks_moderate_confirmation_needed(self
):
270 # The subscription policy requires confirmation and moderation, and the
271 # subscription is not pre-confirmed.
272 self
._mlist
.subscription_policy
= \
273 SubscriptionPolicy
.confirm_then_moderate
274 anne
= self
._user
_manager
.create_address(self
._anne
)
275 workflow
= SubscriptionWorkflow(self
._mlist
, anne
, pre_verified
=True)
276 workflow
.run_thru('confirmation_checks')
277 with patch
.object(workflow
, '_step_send_confirmation') as step
:
279 step
.assert_called_once_with()
281 def test_moderation_checks_pre_approved(self
):
282 # The subscription is pre-approved by the moderator.
283 self
._mlist
.subscription_policy
= SubscriptionPolicy
.moderate
284 anne
= self
._user
_manager
.create_address(self
._anne
)
285 workflow
= SubscriptionWorkflow(self
._mlist
, anne
,
288 workflow
.run_thru('moderation_checks')
289 with patch
.object(workflow
, '_step_do_subscription') as step
:
291 step
.assert_called_once_with()
293 def test_moderation_checks_approval_required(self
):
294 # The moderator must approve the subscription.
295 self
._mlist
.subscription_policy
= SubscriptionPolicy
.moderate
296 anne
= self
._user
_manager
.create_address(self
._anne
)
297 workflow
= SubscriptionWorkflow(self
._mlist
, anne
, pre_verified
=True)
298 workflow
.run_thru('moderation_checks')
299 with patch
.object(workflow
, '_step_get_moderator_approval') as step
:
301 step
.assert_called_once_with()
303 def test_do_subscription(self
):
304 # An open subscription policy plus a pre-verified address means the
305 # user gets subscribed to the mailing list without any further
306 # confirmations or approvals.
307 self
._mlist
.subscription_policy
= SubscriptionPolicy
.open
308 anne
= self
._user
_manager
.create_address(self
._anne
)
309 workflow
= SubscriptionWorkflow(self
._mlist
, anne
, pre_verified
=True)
310 # Consume the entire state machine.
312 # Anne is now a member of the mailing list.
313 member
= self
._mlist
.regular_members
.get_member(self
._anne
)
314 self
.assertEqual(member
.address
, anne
)
316 def test_do_subscription_pre_approved(self
):
317 # An moderation-requiring subscription policy plus a pre-verified and
318 # pre-approved address means the user gets subscribed to the mailing
319 # list without any further confirmations or approvals.
320 self
._mlist
.subscription_policy
= SubscriptionPolicy
.moderate
321 anne
= self
._user
_manager
.create_address(self
._anne
)
322 workflow
= SubscriptionWorkflow(self
._mlist
, anne
,
325 # Consume the entire state machine.
327 # Anne is now a member of the mailing list.
328 member
= self
._mlist
.regular_members
.get_member(self
._anne
)
329 self
.assertEqual(member
.address
, anne
)
331 def test_do_subscription_pre_approved_pre_confirmed(self
):
332 # An moderation-requiring subscription policy plus a pre-verified and
333 # pre-approved address means the user gets subscribed to the mailing
334 # list without any further confirmations or approvals.
335 self
._mlist
.subscription_policy
= \
336 SubscriptionPolicy
.confirm_then_moderate
337 anne
= self
._user
_manager
.create_address(self
._anne
)
338 workflow
= SubscriptionWorkflow(self
._mlist
, anne
,
342 # Consume the entire state machine.
344 # Anne is now a member of the mailing list.
345 member
= self
._mlist
.regular_members
.get_member(self
._anne
)
346 self
.assertEqual(member
.address
, anne
)
348 def test_do_subscription_cleanups(self
):
349 # Once the user is subscribed, the token, and its associated pending
350 # database record will be removed from the database.
351 self
._mlist
.subscription_policy
= SubscriptionPolicy
.open
352 anne
= self
._user
_manager
.create_address(self
._anne
)
353 workflow
= SubscriptionWorkflow(self
._mlist
, anne
,
358 token
= workflow
.token
359 # Consume the entire state machine.
361 # Anne is now a member of the mailing list.
362 member
= self
._mlist
.regular_members
.get_member(self
._anne
)
363 self
.assertEqual(member
.address
, anne
)
364 # The workflow is done, so it has no token.
365 self
.assertIsNone(workflow
.token
)
366 # The pendable associated with the token has been evicted.
367 self
.assertIsNone(getUtility(IPendings
).confirm(token
, expunge
=False))
368 # There is no saved workflow associated with the token.
369 new_workflow
= SubscriptionWorkflow(self
._mlist
)
370 new_workflow
.token
= token
371 new_workflow
.restore()
372 self
.assertIsNone(new_workflow
.which
)
374 def test_moderator_approves(self
):
375 # The workflow runs until moderator approval is required, at which
376 # point the workflow is saved. Once the moderator approves, the
377 # workflow resumes and the user is subscribed.
378 self
._mlist
.subscription_policy
= SubscriptionPolicy
.moderate
379 anne
= self
._user
_manager
.create_address(self
._anne
)
380 workflow
= SubscriptionWorkflow(self
._mlist
, anne
,
383 # Consume the entire state machine.
385 # The user is not currently subscribed to the mailing list.
386 member
= self
._mlist
.regular_members
.get_member(self
._anne
)
387 self
.assertIsNone(member
)
388 # Create a new workflow with the previous workflow's save token, and
389 # restore its state. This models an approved subscription and should
390 # result in the user getting subscribed.
391 approved_workflow
= SubscriptionWorkflow(self
._mlist
)
392 approved_workflow
.token
= workflow
.token
393 approved_workflow
.restore()
394 list(approved_workflow
)
395 # Now the user is subscribed to the mailing list.
396 member
= self
._mlist
.regular_members
.get_member(self
._anne
)
397 self
.assertEqual(member
.address
, anne
)
399 def test_get_moderator_approval_log_on_hold(self
):
400 # When the subscription is held for moderator approval, a message is
402 mark
= LogFileMark('mailman.subscribe')
403 self
._mlist
.subscription_policy
= SubscriptionPolicy
.moderate
404 anne
= self
._user
_manager
.create_address(self
._anne
)
405 workflow
= SubscriptionWorkflow(self
._mlist
, anne
,
408 # Consume the entire state machine.
410 line
= mark
.readline()
413 'test@example.com: held subscription request from anne@example.com'
416 def test_get_moderator_approval_notifies_moderators(self
):
417 # When the subscription is held for moderator approval, and the list
418 # is so configured, a notification is sent to the list moderators.
419 self
._mlist
.admin_immed_notify
= True
420 self
._mlist
.subscription_policy
= SubscriptionPolicy
.moderate
421 anne
= self
._user
_manager
.create_address(self
._anne
)
422 workflow
= SubscriptionWorkflow(self
._mlist
, anne
,
425 # Consume the entire state machine.
427 items
= get_queue_messages('virgin')
428 self
.assertEqual(len(items
), 1)
429 message
= items
[0].msg
430 self
.assertEqual(message
['From'], 'test-owner@example.com')
431 self
.assertEqual(message
['To'], 'test-owner@example.com')
434 'New subscription request to Test from anne@example.com')
435 self
.assertEqual(message
.get_payload(), """\
436 Your authorization is required for a mailing list subscription request
439 For: anne@example.com
440 List: test@example.com""")
442 def test_get_moderator_approval_no_notifications(self
):
443 # When the subscription is held for moderator approval, and the list
444 # is so configured, a notification is sent to the list moderators.
445 self
._mlist
.admin_immed_notify
= False
446 self
._mlist
.subscription_policy
= SubscriptionPolicy
.moderate
447 anne
= self
._user
_manager
.create_address(self
._anne
)
448 workflow
= SubscriptionWorkflow(self
._mlist
, anne
,
451 # Consume the entire state machine.
453 items
= get_queue_messages('virgin')
454 self
.assertEqual(len(items
), 0)
456 def test_send_confirmation(self
):
457 # A confirmation message gets sent when the address is not verified.
458 anne
= self
._user
_manager
.create_address(self
._anne
)
459 self
.assertIsNone(anne
.verified_on
)
460 # Run the workflow to model the confirmation step.
461 workflow
= SubscriptionWorkflow(self
._mlist
, anne
)
463 items
= get_queue_messages('virgin')
464 self
.assertEqual(len(items
), 1)
465 message
= items
[0].msg
466 token
= workflow
.token
467 self
.assertEqual(message
['Subject'], 'confirm {}'.format(token
))
469 message
['From'], 'test-confirm+{}@example.com'.format(token
))
471 def test_send_confirmation_pre_confirmed(self
):
472 # A confirmation message gets sent when the address is not verified
473 # but the subscription is pre-confirmed.
474 anne
= self
._user
_manager
.create_address(self
._anne
)
475 self
.assertIsNone(anne
.verified_on
)
476 # Run the workflow to model the confirmation step.
477 workflow
= SubscriptionWorkflow(self
._mlist
, anne
, pre_confirmed
=True)
479 items
= get_queue_messages('virgin')
480 self
.assertEqual(len(items
), 1)
481 message
= items
[0].msg
482 token
= workflow
.token
484 message
['Subject'], 'confirm {}'.format(workflow
.token
))
486 message
['From'], 'test-confirm+{}@example.com'.format(token
))
488 def test_send_confirmation_pre_verified(self
):
489 # A confirmation message gets sent even when the address is verified
490 # when the subscription must be confirmed.
491 self
._mlist
.subscription_policy
= SubscriptionPolicy
.confirm
492 anne
= self
._user
_manager
.create_address(self
._anne
)
493 self
.assertIsNone(anne
.verified_on
)
494 # Run the workflow to model the confirmation step.
495 workflow
= SubscriptionWorkflow(self
._mlist
, anne
, pre_verified
=True)
497 items
= get_queue_messages('virgin')
498 self
.assertEqual(len(items
), 1)
499 message
= items
[0].msg
500 token
= workflow
.token
502 message
['Subject'], 'confirm {}'.format(workflow
.token
))
504 message
['From'], 'test-confirm+{}@example.com'.format(token
))
506 def test_do_confirm_verify_address(self
):
507 # The address is not yet verified, nor are we pre-verifying. A
508 # confirmation message will be sent. When the user confirms their
509 # subscription request, the address will end up being verified.
510 anne
= self
._user
_manager
.create_address(self
._anne
)
511 self
.assertIsNone(anne
.verified_on
)
512 # Run the workflow to model the confirmation step.
513 workflow
= SubscriptionWorkflow(self
._mlist
, anne
)
515 # The address is still not verified.
516 self
.assertIsNone(anne
.verified_on
)
517 confirm_workflow
= SubscriptionWorkflow(self
._mlist
)
518 confirm_workflow
.token
= workflow
.token
519 confirm_workflow
.restore()
520 confirm_workflow
.run_thru('do_confirm_verify')
521 # The address is now verified.
522 self
.assertIsNotNone(anne
.verified_on
)
524 def test_do_confirmation_subscribes_user(self
):
525 # Subscriptions to the mailing list must be confirmed. Once that's
526 # done, the user's address (which is not initially verified) gets
527 # subscribed to the mailing list.
528 self
._mlist
.subscription_policy
= SubscriptionPolicy
.confirm
529 anne
= self
._user
_manager
.create_address(self
._anne
)
530 self
.assertIsNone(anne
.verified_on
)
531 workflow
= SubscriptionWorkflow(self
._mlist
, anne
)
533 self
.assertIsNone(self
._mlist
.regular_members
.get_member(self
._anne
))
534 confirm_workflow
= SubscriptionWorkflow(self
._mlist
)
535 confirm_workflow
.token
= workflow
.token
536 confirm_workflow
.restore()
537 list(confirm_workflow
)
538 self
.assertIsNotNone(anne
.verified_on
)
540 self
._mlist
.regular_members
.get_member(self
._anne
).address
, anne
)