class IndividualRequest(_ModerationBase): """Resource for moderating a membership change.""" def __init__(self, mlist, token): super().__init__() self._mlist = mlist self._registrar = IRegistrar(self._mlist) self._token = token def on_get(self, request, response): # Get the pended record associated with this token, if it exists in # the pending table. try: resource = self._resource_as_dict(self._token) assert resource is not None, resource except LookupError: not_found(response) return okay(response, etag(resource)) def on_post(self, request, response): try: validator = Validator(action=enum_validator(Action)) arguments = validator(request) except ValueError as error: bad_request(response, str(error)) return action = arguments['action'] if action is Action.defer: # At least see if the token is in the database. pendable = self._pendings.confirm(self._token, expunge=False) if pendable is None: not_found(response) else: no_content(response) elif action is Action.accept: try: self._registrar.confirm(self._token) except LookupError: not_found(response) else: no_content(response) elif action is Action.discard: # At least see if the token is in the database. pendable = self._pendings.confirm(self._token, expunge=True) if pendable is None: not_found(response) else: no_content(response) elif action is Action.reject: # Like discard but sends a rejection notice to the user. pendable = self._pendings.confirm(self._token, expunge=True) if pendable is None: not_found(response) else: no_content(response) send_rejection( self._mlist, _('Subscription request'), pendable['email'], _('[No reason given]'))
class IndividualRequest(_ModerationBase): """Resource for moderating a membership change.""" def __init__(self, mlist, token): super().__init__() self._mlist = mlist self._registrar = IRegistrar(self._mlist) self._token = token def on_get(self, request, response): # Get the pended record associated with this token, if it exists in # the pending table. try: resource = self._resource_as_dict(self._token) assert resource is not None, resource except LookupError: not_found(response) return okay(response, etag(resource)) def on_post(self, request, response): try: validator = Validator(action=enum_validator(Action)) arguments = validator(request) except ValueError as error: bad_request(response, str(error)) return action = arguments['action'] if action in (Action.defer, Action.hold): # At least see if the token is in the database. pendable = self._pendings.confirm(self._token, expunge=False) if pendable is None: not_found(response) else: no_content(response) elif action is Action.accept: try: self._registrar.confirm(self._token) except LookupError: not_found(response) except AlreadySubscribedError: conflict(response, 'Already subscribed') else: no_content(response) elif action is Action.discard: # At least see if the token is in the database. pendable = self._pendings.confirm(self._token, expunge=True) if pendable is None: not_found(response) else: no_content(response) else: assert action is Action.reject, action # Like discard but sends a rejection notice to the user. pendable = self._pendings.confirm(self._token, expunge=True) if pendable is None: not_found(response) else: no_content(response) send_rejection(self._mlist, _('Subscription request'), pendable['email'], _('[No reason given]'))
def setUp(self): with transaction(): self._mlist = create_list('*****@*****.**') self._registrar = IRegistrar(self._mlist) manager = getUtility(IUserManager) self._anne = manager.create_address('*****@*****.**', 'Anne Person') self._bart = manager.make_user('*****@*****.**', 'Bart Person') set_preferred(self._bart)
def setUp(self): self._commandq = config.switchboards['command'] self._runner = make_testable_runner(CommandRunner, 'command') with transaction(): # Register a subscription requiring confirmation. self._mlist = create_list('*****@*****.**') self._mlist.send_welcome_message = False anne = getUtility(IUserManager).create_address('*****@*****.**') registrar = IRegistrar(self._mlist) self._token, token_owner, member = registrar.register(anne)
def setUp(self): with transaction(): self._mlist = create_list('*****@*****.**') self._registrar = IRegistrar(self._mlist) manager = getUtility(IUserManager) self._anne = manager.create_address('*****@*****.**', 'Anne Person') self._bart = manager.make_user('*****@*****.**', 'Bart Person') preferred = list(self._bart.addresses)[0] preferred.verified_on = now() self._bart.preferred_address = preferred
def process(self, mlist, msg, msgdata, arguments, results): """See `IEmailCommand`.""" # The token must be in the arguments. if len(arguments) == 0: print(_('No confirmation token found'), file=results) return ContinueProcessing.no # Make sure we don't try to confirm the same token more than once. token = arguments[0] tokens = getattr(results, 'confirms', set()) if token in tokens: # Do not try to confirm this one again. return ContinueProcessing.yes tokens.add(token) results.confirms = tokens try: token, token_owner, member = IRegistrar(mlist).confirm(token) if token is None: assert token_owner is TokenOwner.no_one, token_owner assert member is not None, member succeeded = True else: assert token_owner is not TokenOwner.no_one, token_owner assert member is None, member succeeded = False except LookupError: # The token must not exist in the database. succeeded = False if succeeded: print(_('Confirmed'), file=results) return ContinueProcessing.yes print(_('Confirmation token did not match'), file=results) return ContinueProcessing.no
def setUp(self): self._mlist = create_list('*****@*****.**') anne = getUtility(IUserManager).create_address('*****@*****.**', 'Anne Person') self._token, token_owner, member = IRegistrar( self._mlist).register(anne) self._command = Confirm() # Clear the virgin queue. get_queue_messages('virgin')
def setUp(self): with transaction(): self._mlist = create_list("*****@*****.**") self._registrar = IRegistrar(self._mlist) manager = getUtility(IUserManager) self._anne = manager.create_address("*****@*****.**", "Anne Person") self._bart = manager.make_user("*****@*****.**", "Bart Person") preferred = list(self._bart.addresses)[0] preferred.verified_on = now() self._bart.preferred_address = preferred
def setUp(self): with transaction(): self._mlist = create_list('*****@*****.**') self._registrar = IRegistrar(self._mlist) manager = getUtility(IUserManager) self._anne = manager.create_address( '*****@*****.**', 'Anne Person') self._bart = manager.make_user( '*****@*****.**', 'Bart Person') set_preferred(self._bart)
def test_duplicate_pending_subscription(self): # Issue #199 - a member's subscription is already pending and they try # to subscribe again. registrar = IRegistrar(self._mlist) with transaction(): self._mlist.subscription_policy = SubscriptionPolicy.moderate anne = self._usermanager.create_address('*****@*****.**') token, token_owner, member = registrar.register( anne, pre_verified=True, pre_confirmed=True) self.assertEqual(token_owner, TokenOwner.moderator) self.assertIsNone(member) with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com', 'pre_verified': True, 'pre_confirmed': True, }) self.assertEqual(cm.exception.code, 409) self.assertEqual(cm.exception.reason, b'Subscription request already pending')
def _confirm(self): # There will be two messages in the queue - the confirmation messages, # and a reply to Anne notifying her of the status of her command # email. We need to dig the confirmation token out of the Subject # header of the latter so that we can confirm the subscription. messages = get_queue_messages('virgin', sort_on='subject') self.assertEqual(len(messages), 2) subject_words = str(messages[1].msg['subject']).split() self.assertEqual(subject_words[0], 'confirm') token = subject_words[1] token, token_owner, rmember = IRegistrar(self._mlist).confirm(token) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) # Now, make sure that Anne is a member of the list and is receiving # digest deliveries. members = getUtility(ISubscriptionService).find_members( '*****@*****.**') self.assertEqual(len(members), 1) self.assertEqual(rmember, members[0]) return rmember
class TestUnsubscription(unittest.TestCase): """Test unsubscription requests.""" layer = SMTPLayer def setUp(self): self._mlist = create_list('*****@*****.**') self._registrar = IRegistrar(self._mlist) def test_unsubscribe_defer(self): # When unsubscriptions must be approved by the moderator, but the # moderator defers this decision. anne = getUtility(IUserManager).create_address( '*****@*****.**', 'Anne Person') token, token_owner, member = self._registrar.register( anne, pre_verified=True, pre_confirmed=True, pre_approved=True) self.assertIsNone(token) self.assertEqual(member.address.email, '*****@*****.**') # Now hold and handle an unsubscription request. token = hold_unsubscription(self._mlist, '*****@*****.**') handle_unsubscription(self._mlist, token, Action.defer)
class TestUnsubscription(unittest.TestCase): """Test unsubscription requests.""" layer = SMTPLayer def setUp(self): self._mlist = create_list('*****@*****.**') self._registrar = IRegistrar(self._mlist) def test_unsubscribe_defer(self): # When unsubscriptions must be approved by the moderator, but the # moderator defers this decision. anne = getUtility(IUserManager).create_address('*****@*****.**', 'Anne Person') token, token_owner, member = self._registrar.register( anne, pre_verified=True, pre_confirmed=True, pre_approved=True) self.assertIsNone(token) self.assertEqual(member.address.email, '*****@*****.**') # Now hold and handle an unsubscription request. token = hold_unsubscription(self._mlist, '*****@*****.**') handle_unsubscription(self._mlist, token, Action.defer)
def process(self, mlist, msg, msgdata, arguments, results): """See `IEmailCommand`.""" # Parse the arguments. delivery_mode = self._parse_arguments(arguments, results) if delivery_mode is ContinueProcessing.no: return ContinueProcessing.no display_name, email = parseaddr(msg['from']) # Address could be None or the empty string. if not email: email = msg.sender if not email: print(_('$self.name: No valid address found to subscribe'), file=results) return ContinueProcessing.no if isinstance(email, bytes): email = email.decode('ascii') # Have we already seen one join request from this user during the # processing of this email? joins = getattr(results, 'joins', set()) if email in joins: # Do not register this join. return ContinueProcessing.yes joins.add(email) results.joins = joins person = formataddr((display_name, email)) # noqa # Is this person already a member of the list? Search for all # matching memberships. members = getUtility(ISubscriptionService).find_members( email, mlist.list_id, MemberRole.member) if len(members) > 0: print(_('$person is already a member'), file=results) return ContinueProcessing.yes subscriber = match_subscriber(email, display_name) IRegistrar(mlist).register(subscriber) print(_('Confirmation email sent to $person'), file=results) return ContinueProcessing.yes
def __init__(self, mlist, token): super().__init__() self._mlist = mlist self._registrar = IRegistrar(self._mlist) self._token = token
class TestSubscriptionModeration(unittest.TestCase): layer = RESTLayer maxDiff = None def setUp(self): with transaction(): self._mlist = create_list("*****@*****.**") self._registrar = IRegistrar(self._mlist) manager = getUtility(IUserManager) self._anne = manager.create_address("*****@*****.**", "Anne Person") self._bart = manager.make_user("*****@*****.**", "Bart Person") preferred = list(self._bart.addresses)[0] preferred.verified_on = now() self._bart.preferred_address = preferred def test_no_such_list(self): # Try to get the requests of a nonexistent list. with self.assertRaises(HTTPError) as cm: call_api("http://*****:*****@example.com/" "requests") self.assertEqual(cm.exception.code, 404) def test_no_such_subscription_token(self): # Bad request when the token is not in the database. with self.assertRaises(HTTPError) as cm: call_api("http://*****:*****@example.com/" "requests/missing") self.assertEqual(cm.exception.code, 404) def test_bad_subscription_action(self): # POSTing to a held message with a bad action. token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Let's try to handle her request, but with a bogus action. url = "http://*****:*****@example.com/requests/{}" with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action="bogus")) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.msg, b"Cannot convert parameters: action") def test_list_held_requests(self): # We can view all the held requests. with transaction(): token_1, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNotNone(token_1) self.assertIsNone(member) token_2, token_owner, member = self._registrar.register(self._bart) self.assertIsNotNone(token_2) self.assertIsNone(member) content, response = call_api("http://*****:*****@example.com/requests") self.assertEqual(response.status, 200) self.assertEqual(content["total_size"], 2) tokens = set(json["token"] for json in content["entries"]) self.assertEqual(tokens, {token_1, token_2}) emails = set(json["email"] for json in content["entries"]) self.assertEqual(emails, {"*****@*****.**", "*****@*****.**"}) def test_individual_request(self): # We can view an individual request. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNotNone(token) self.assertIsNone(member) url = "http://*****:*****@example.com/requests/{}" content, response = call_api(url.format(token)) self.assertEqual(response.status, 200) self.assertEqual(content["token"], token) self.assertEqual(content["token_owner"], token_owner.name) self.assertEqual(content["email"], "*****@*****.**") def test_accept(self): # POST to the request to accept it. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = "http://*****:*****@example.com/requests/{}" content, response = call_api(url.format(token), dict(action="accept")) self.assertEqual(response.status, 204) # Anne is a member. self.assertEqual(self._mlist.members.get_member("*****@*****.**").address, self._anne) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action="accept")) self.assertEqual(cm.exception.code, 404) def test_accept_bad_token(self): # Try to accept a request with a bogus token. with self.assertRaises(HTTPError) as cm: call_api("http://*****:*****@example.com" "/requests/bogus", dict(action="accept")) self.assertEqual(cm.exception.code, 404) def test_accept_by_moderator_clears_request_queue(self): # After accepting a message held for moderator approval, there are no # more requests to handle. # # We start with nothing in the queue. content, response = call_api("http://*****:*****@example.com/requests") self.assertEqual(content["total_size"], 0) # Anne tries to subscribe to a list that only requests moderator # approval. with transaction(): self._mlist.subscription_policy = SubscriptionPolicy.moderate token, token_owner, member = self._registrar.register(self._anne, pre_verified=True, pre_confirmed=True) # There's now one request in the queue, and it's waiting on moderator # approval. content, response = call_api("http://*****:*****@example.com/requests") self.assertEqual(content["total_size"], 1) json = content["entries"][0] self.assertEqual(json["token_owner"], "moderator") self.assertEqual(json["email"], "*****@*****.**") # The moderator approves the request. url = "http://*****:*****@example.com/requests/{}" content, response = call_api(url.format(token), {"action": "accept"}) self.assertEqual(response.status, 204) # And now the request queue is empty. content, response = call_api("http://*****:*****@example.com/requests") self.assertEqual(content["total_size"], 0) def test_discard(self): # POST to the request to discard it. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = "http://*****:*****@example.com/requests/{}" content, response = call_api(url.format(token), dict(action="discard")) self.assertEqual(response.status, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member("*****@*****.**")) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action="discard")) self.assertEqual(cm.exception.code, 404) def test_defer(self): # Defer the decision for some other moderator. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = "http://*****:*****@example.com/requests/{}" content, response = call_api(url.format(token), dict(action="defer")) self.assertEqual(response.status, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member("*****@*****.**")) # The request URL still exists. content, response = call_api(url.format(token), dict(action="defer")) self.assertEqual(response.status, 204) # And now we can accept it. content, response = call_api(url.format(token), dict(action="accept")) self.assertEqual(response.status, 204) # Anne is a member. self.assertEqual(self._mlist.members.get_member("*****@*****.**").address, self._anne) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action="accept")) self.assertEqual(cm.exception.code, 404) def test_defer_bad_token(self): # Try to accept a request with a bogus token. with self.assertRaises(HTTPError) as cm: call_api("http://*****:*****@example.com" "/requests/bogus", dict(action="defer")) self.assertEqual(cm.exception.code, 404) def test_reject(self): # POST to the request to reject it. This leaves a bounce message in # the virgin queue. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Clear out the virgin queue, which currently contains the # confirmation message sent to Anne. get_queue_messages("virgin") url = "http://*****:*****@example.com/requests/{}" content, response = call_api(url.format(token), dict(action="reject")) self.assertEqual(response.status, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member("*****@*****.**")) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action="reject")) self.assertEqual(cm.exception.code, 404) # And the rejection message to Anne is now in the virgin queue. items = get_queue_messages("virgin") self.assertEqual(len(items), 1) message = items[0].msg self.assertEqual(message["From"], "*****@*****.**") self.assertEqual(message["To"], "*****@*****.**") self.assertEqual(message["Subject"], 'Request to mailing list "Ant" rejected') def test_reject_bad_token(self): # Try to accept a request with a bogus token. with self.assertRaises(HTTPError) as cm: call_api("http://*****:*****@example.com" "/requests/bogus", dict(action="reject")) self.assertEqual(cm.exception.code, 404)
def on_post(self, request, response): """Create a new member.""" try: validator = Validator( list_id=str, subscriber=subscriber_validator(self.api), display_name=str, delivery_mode=enum_validator(DeliveryMode), role=enum_validator(MemberRole), pre_verified=bool, pre_confirmed=bool, pre_approved=bool, _optional=('delivery_mode', 'display_name', 'role', 'pre_verified', 'pre_confirmed', 'pre_approved')) arguments = validator(request) except ValueError as error: bad_request(response, str(error)) return # Dig the mailing list out of the arguments. list_id = arguments.pop('list_id') mlist = getUtility(IListManager).get_by_list_id(list_id) if mlist is None: bad_request(response, b'No such list') return # Figure out what kind of subscriber is being registered. Either it's # a user via their preferred email address or it's an explicit address. # If it's a UUID, then it must be associated with an existing user. subscriber = arguments.pop('subscriber') user_manager = getUtility(IUserManager) # We use the display name if there is one. display_name = arguments.pop('display_name', '') if isinstance(subscriber, UUID): user = user_manager.get_user_by_id(subscriber) if user is None: bad_request(response, b'No such user') return subscriber = user else: # This must be an email address. See if there's an existing # address object associated with this email. address = user_manager.get_address(subscriber) if address is None: # Create a new address, which of course will not be validated. address = user_manager.create_address( subscriber, display_name) subscriber = address # What role are we subscribing? Regular members go through the # subscription policy workflow while owners, moderators, and # nonmembers go through the legacy API for now. role = arguments.pop('role', MemberRole.member) if role is MemberRole.member: # Get the pre_ flags for the subscription workflow. pre_verified = arguments.pop('pre_verified', False) pre_confirmed = arguments.pop('pre_confirmed', False) pre_approved = arguments.pop('pre_approved', False) # Now we can run the registration process until either the # subscriber is subscribed, or the workflow is paused for # verification, confirmation, or approval. registrar = IRegistrar(mlist) try: token, token_owner, member = registrar.register( subscriber, pre_verified=pre_verified, pre_confirmed=pre_confirmed, pre_approved=pre_approved) except AlreadySubscribedError: conflict(response, b'Member already subscribed') return except MissingPreferredAddressError: bad_request(response, b'User has no preferred address') return except MembershipIsBannedError: bad_request(response, b'Membership is banned') return except SubscriptionPendingError: conflict(response, b'Subscription request already pending') return if token is None: assert token_owner is TokenOwner.no_one, token_owner # The subscription completed. Let's get the resulting member # and return the location to the new member. Member ids are # UUIDs and need to be converted to URLs because JSON doesn't # directly support UUIDs. member_id = self.api.from_uuid(member.member_id) location = self.api.path_to('members/{}'.format(member_id)) created(response, location) return # The member could not be directly subscribed because there are # some out-of-band steps that need to be completed. E.g. the user # must confirm their subscription or the moderator must approve # it. In this case, an HTTP 202 Accepted is exactly the code that # we should use, and we'll return both the confirmation token and # the "token owner" so the client knows who should confirm it. assert token is not None, token assert token_owner is not TokenOwner.no_one, token_owner assert member is None, member content = dict(token=token, token_owner=token_owner.name) accepted(response, etag(content)) return # 2015-04-15 BAW: We're subscribing some role other than a regular # member. Use the legacy API for this for now. assert role in (MemberRole.owner, MemberRole.moderator, MemberRole.nonmember) # 2015-04-15 BAW: We're limited to using an email address with this # legacy API, so if the subscriber is a user, the user must have a # preferred address, which we'll use, even though it will subscribe # the explicit address. It is an error if the user does not have a # preferred address. # # If the subscriber is an address object, just use that. if IUser.providedBy(subscriber): if subscriber.preferred_address is None: bad_request(response, b'User without preferred address') return email = subscriber.preferred_address.email else: assert IAddress.providedBy(subscriber) email = subscriber.email delivery_mode = arguments.pop('delivery_mode', DeliveryMode.regular) record = RequestRecord(email, display_name, delivery_mode) try: member = add_member(mlist, record, role) except MembershipIsBannedError: bad_request(response, b'Membership is banned') return except AlreadySubscribedError: bad_request(response, '{} is already an {} of {}'.format( email, role.name, mlist.fqdn_listname)) return # The subscription completed. Let's get the resulting member # and return the location to the new member. Member ids are # UUIDs and need to be converted to URLs because JSON doesn't # directly support UUIDs. member_id = self.api.from_uuid(member.member_id) location = self.api.path_to('members/{}'.format(member_id)) created(response, location)
def setUp(self): self._mlist = create_list('*****@*****.**') self._registrar = IRegistrar(self._mlist) self._pendings = getUtility(IPendings) self._anne = getUtility(IUserManager).create_address( '*****@*****.**')
class TestRegistrar(unittest.TestCase): """Test registration.""" layer = ConfigLayer def setUp(self): self._mlist = create_list('*****@*****.**') self._registrar = IRegistrar(self._mlist) self._pendings = getUtility(IPendings) self._anne = getUtility(IUserManager).create_address( '*****@*****.**') def test_initial_conditions(self): # Registering a subscription request provides a unique token associated # with a pendable, and the owner of the token. self.assertEqual(self._pendings.count, 0) token, token_owner, member = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(member) self.assertEqual(self._pendings.count, 1) record = self._pendings.confirm(token, expunge=False) self.assertEqual(record['list_id'], self._mlist.list_id) self.assertEqual(record['email'], '*****@*****.**') def test_subscribe(self): # Registering a subscription request where no confirmation or # moderation steps are needed, leaves us with no token or owner, since # there's nothing more to do. self._mlist.subscription_policy = SubscriptionPolicy.open self._anne.verified_on = now() token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) # There's nothing to confirm. record = self._pendings.confirm(token, expunge=False) self.assertIsNone(record) def test_no_such_token(self): # Given a token which is not in the database, a LookupError is raised. self._registrar.register(self._anne) self.assertRaises(LookupError, self._registrar.confirm, 'not-a-token') def test_confirm_because_verify(self): # We have a subscription request which requires the user to confirm # (because she does not have a verified address), but not the moderator # to approve. Running the workflow gives us a token. Confirming the # token subscribes the user. self._mlist.subscription_policy = SubscriptionPolicy.open token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription. token, token_owner, rmember = self._registrar.confirm(token) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_confirm_because_confirm(self): # We have a subscription request which requires the user to confirm # (because of list policy), but not the moderator to approve. Running # the workflow gives us a token. Confirming the token subscribes the # user. self._mlist.subscription_policy = SubscriptionPolicy.confirm self._anne.verified_on = now() token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription. token, token_owner, rmember = self._registrar.confirm(token) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_confirm_because_moderation(self): # We have a subscription request which requires the moderator to # approve. Running the workflow gives us a token. Confirming the # token subscribes the user. self._mlist.subscription_policy = SubscriptionPolicy.moderate self._anne.verified_on = now() token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.moderator) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription. token, token_owner, rmember = self._registrar.confirm(token) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_confirm_because_confirm_then_moderation(self): # We have a subscription request which requires the user to confirm # (because she does not have a verified address) and the moderator to # approve. Running the workflow gives us a token. Confirming the # token runs the workflow a little farther, but still gives us a # token. Confirming again subscribes the user. self._mlist.subscription_policy = ( SubscriptionPolicy.confirm_then_moderate) self._anne.verified_on = now() # Runs until subscription confirmation. token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription, and wait for the moderator to approve # the subscription. She is still not subscribed. new_token, token_owner, rmember = self._registrar.confirm(token) # The new token, used for the moderator to approve the message, is not # the same as the old token. self.assertNotEqual(new_token, token) self.assertIsNotNone(new_token) self.assertEqual(token_owner, TokenOwner.moderator) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Confirm once more, this time as the moderator approving the # subscription. Now she's a member. token, token_owner, rmember = self._registrar.confirm(new_token) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_confirm_then_moderate_with_different_tokens(self): # Ensure that the confirmation token the user sees when they have to # confirm their subscription is different than the token the moderator # sees when they approve the subscription. This prevents the user # from using a replay attack to subvert moderator approval. self._mlist.subscription_policy = ( SubscriptionPolicy.confirm_then_moderate) self._anne.verified_on = now() # Runs until subscription confirmation. token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription, and wait for the moderator to approve # the subscription. She is still not subscribed. new_token, token_owner, rmember = self._registrar.confirm(token) # The status is not true because the user has not yet been subscribed # to the mailing list. self.assertIsNotNone(new_token) self.assertEqual(token_owner, TokenOwner.moderator) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # The new token is different than the old token. self.assertNotEqual(token, new_token) # Trying to confirm with the old token does not work. self.assertRaises(LookupError, self._registrar.confirm, token) # Confirm once more, this time with the new token, as the moderator # approving the subscription. Now she's a member. done_token, token_owner, rmember = self._registrar.confirm(new_token) # The token is None, signifying that the member has been subscribed. self.assertIsNone(done_token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_discard_waiting_for_confirmation(self): # While waiting for a user to confirm their subscription, we discard # the workflow. self._mlist.subscription_policy = SubscriptionPolicy.confirm self._anne.verified_on = now() # Runs until subscription confirmation. token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now discard the subscription request. self._registrar.discard(token) # Trying to confirm the token now results in an exception. self.assertRaises(LookupError, self._registrar.confirm, token) def test_admin_notify_mchanges(self): # When a user gets subscribed via the subscription policy workflow, # the list administrators get an email notification. self._mlist.subscription_policy = SubscriptionPolicy.open self._mlist.admin_notify_mchanges = True self._mlist.send_welcome_message = False token, token_owner, member = self._registrar.register( self._anne, pre_verified=True) # Anne is now a member. self.assertEqual(member.address.email, '*****@*****.**') # And there's a notification email waiting for Bart. items = get_queue_messages('virgin', expected_count=1) message = items[0].msg self.assertEqual(message['To'], '*****@*****.**') self.assertEqual(message['Subject'], 'Ant subscription notification') self.assertEqual(message.get_payload(), """\ [email protected] has been successfully subscribed to Ant.""") def test_no_admin_notify_mchanges(self): # Even when a user gets subscribed via the subscription policy # workflow, the list administrators won't get an email notification if # they don't want one. self._mlist.subscription_policy = SubscriptionPolicy.open self._mlist.admin_notify_mchanges = False self._mlist.send_welcome_message = False # Bart is an administrator of the mailing list. bart = getUtility(IUserManager).create_address( '*****@*****.**', 'Bart Person') self._mlist.subscribe(bart, MemberRole.owner) token, token_owner, member = self._registrar.register( self._anne, pre_verified=True) # Anne is now a member. self.assertEqual(member.address.email, '*****@*****.**') # There's no notification email waiting for Bart. get_queue_messages('virgin', expected_count=0)
def setUp(self): self._mlist = create_list('*****@*****.**') self._registrar = IRegistrar(self._mlist)
class TestSubscriptionModeration(unittest.TestCase): layer = RESTLayer maxDiff = None def setUp(self): with transaction(): self._mlist = create_list('*****@*****.**') self._registrar = IRegistrar(self._mlist) manager = getUtility(IUserManager) self._anne = manager.create_address('*****@*****.**', 'Anne Person') self._bart = manager.make_user('*****@*****.**', 'Bart Person') set_preferred(self._bart) def test_no_such_list(self): # Try to get the requests of a nonexistent list. with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com/' 'requests') self.assertEqual(cm.exception.code, 404) def test_no_such_subscription_token(self): # Bad request when the token is not in the database. with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com/' 'requests/missing') self.assertEqual(cm.exception.code, 404) def test_bad_subscription_action(self): # POSTing to a held message with a bad action. token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Let's try to handle her request, but with a bogus action. url = 'http://*****:*****@example.com/requests/{}' with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='bogus', )) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.msg, b'Cannot convert parameters: action') def test_list_held_requests(self): # We can view all the held requests. with transaction(): token_1, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNotNone(token_1) self.assertIsNone(member) token_2, token_owner, member = self._registrar.register(self._bart) self.assertIsNotNone(token_2) self.assertIsNone(member) content, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(response.status, 200) self.assertEqual(content['total_size'], 2) tokens = set(json['token'] for json in content['entries']) self.assertEqual(tokens, {token_1, token_2}) emails = set(json['email'] for json in content['entries']) self.assertEqual(emails, {'*****@*****.**', '*****@*****.**'}) def test_individual_request(self): # We can view an individual request. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNotNone(token) self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' content, response = call_api(url.format(token)) self.assertEqual(response.status, 200) self.assertEqual(content['token'], token) self.assertEqual(content['token_owner'], token_owner.name) self.assertEqual(content['email'], '*****@*****.**') def test_accept(self): # POST to the request to accept it. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' content, response = call_api(url.format(token), dict(action='accept', )) self.assertEqual(response.status, 204) # Anne is a member. self.assertEqual( self._mlist.members.get_member('*****@*****.**').address, self._anne) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='accept', )) self.assertEqual(cm.exception.code, 404) def test_accept_already_subscribed(self): # POST to a subscription request, but the user is already subscribed. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Make Anne already a member. self._mlist.subscribe(self._anne) # Accept the pending subscription, which raises an error. url = 'http://*****:*****@example.com' '/requests/bogus', dict(action='accept')) self.assertEqual(cm.exception.code, 404) def test_accept_by_moderator_clears_request_queue(self): # After accepting a message held for moderator approval, there are no # more requests to handle. # # We start with nothing in the queue. content, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(content['total_size'], 0) # Anne tries to subscribe to a list that only requests moderator # approval. with transaction(): self._mlist.subscription_policy = SubscriptionPolicy.moderate token, token_owner, member = self._registrar.register( self._anne, pre_verified=True, pre_confirmed=True) # There's now one request in the queue, and it's waiting on moderator # approval. content, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(content['total_size'], 1) json = content['entries'][0] self.assertEqual(json['token_owner'], 'moderator') self.assertEqual(json['email'], '*****@*****.**') # The moderator approves the request. url = 'http://*****:*****@example.com/requests/{}' content, response = call_api(url.format(token), {'action': 'accept'}) self.assertEqual(response.status, 204) # And now the request queue is empty. content, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(content['total_size'], 0) def test_discard(self): # POST to the request to discard it. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' content, response = call_api(url.format(token), dict(action='discard', )) self.assertEqual(response.status, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='discard', )) self.assertEqual(cm.exception.code, 404) def test_defer(self): # Defer the decision for some other moderator. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' content, response = call_api(url.format(token), dict(action='defer', )) self.assertEqual(response.status, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL still exists. content, response = call_api(url.format(token), dict(action='defer', )) self.assertEqual(response.status, 204) # And now we can accept it. content, response = call_api(url.format(token), dict(action='accept', )) self.assertEqual(response.status, 204) # Anne is a member. self.assertEqual( self._mlist.members.get_member('*****@*****.**').address, self._anne) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='accept', )) self.assertEqual(cm.exception.code, 404) def test_defer_bad_token(self): # Try to accept a request with a bogus token. with self.assertRaises(HTTPError) as cm: call_api( 'http://*****:*****@example.com' '/requests/bogus', dict(action='defer')) self.assertEqual(cm.exception.code, 404) def test_reject(self): # POST to the request to reject it. This leaves a bounce message in # the virgin queue. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Clear out the virgin queue, which currently contains the # confirmation message sent to Anne. get_queue_messages('virgin') url = 'http://*****:*****@example.com/requests/{}' content, response = call_api(url.format(token), dict(action='reject', )) self.assertEqual(response.status, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='reject', )) self.assertEqual(cm.exception.code, 404) # And the rejection message to Anne is now in the virgin queue. items = get_queue_messages('virgin') self.assertEqual(len(items), 1) message = items[0].msg self.assertEqual(message['From'], '*****@*****.**') self.assertEqual(message['To'], '*****@*****.**') self.assertEqual(message['Subject'], 'Request to mailing list "Ant" rejected') def test_reject_bad_token(self): # Try to accept a request with a bogus token. with self.assertRaises(HTTPError) as cm: call_api( 'http://*****:*****@example.com' '/requests/bogus', dict(action='reject')) self.assertEqual(cm.exception.code, 404) def test_hold_keeps_holding(self): # POST to the request to continue holding it. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Clear out the virgin queue, which currently contains the # confirmation message sent to Anne. get_queue_messages('virgin') url = 'http://*****:*****@example.com/requests/{}' content, response = call_api(url.format(token), dict(action='hold', )) self.assertEqual(response.status, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL still exists. content, response = call_api(url.format(token), dict(action='defer', )) self.assertEqual(response.status, 204) def test_subscribe_other_role_with_no_preferred_address(self): with transaction(): cate = getUtility(IUserManager).create_user('*****@*****.**') with self.assertRaises(HTTPError) as cm: call_api( 'http://*****:*****@example.com') with self.assertRaises(HTTPError) as cm: call_api( 'http://*****:*****@example.com', 'role': 'moderator', }) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, b'Membership is banned')
def test_confirm_then_moderate_workflow(self): # Issue #114 describes a problem when confirming the moderation email. self._mlist.subscription_policy = ( SubscriptionPolicy.confirm_then_moderate) bart = getUtility(IUserManager).create_address('*****@*****.**', 'Bart Person') # Clear any previously queued confirmation messages. get_queue_messages('virgin') self._token, token_owner, member = IRegistrar( self._mlist).register(bart) # There should now be one email message in the virgin queue, i.e. the # confirmation message sent to Bart. items = get_queue_messages('virgin', expected_count=1) msg = items[0].msg # Confirmations come first, so this one goes to the subscriber. self.assertEqual(msg['to'], '*****@*****.**') confirm, token = str(msg['subject']).split() self.assertEqual(confirm, 'confirm') self.assertEqual(token, self._token) # Craft a confirmation response with the expected tokens. user_response = Message() user_response['From'] = '*****@*****.**' user_response['To'] = 'test-confirm+{}@example.com'.format(token) user_response['Subject'] = 'Re: confirm {}'.format(token) user_response.set_payload('') # Process the message through the command runner. config.switchboards['command'].enqueue(user_response, listid='test.example.com') make_testable_runner(CommandRunner, 'command').run() # There are now two messages in the virgin queue. One is going to the # subscriber containing the results of their confirmation message, and # the other is to the moderators informing them that they need to # handle the moderation queue. items = get_queue_messages('virgin', expected_count=2) if items[0].msg['to'] == '*****@*****.**': results = items[0].msg moderator_msg = items[1].msg else: results = items[1].msg moderator_msg = items[0].msg # Check the moderator message first. self.assertEqual(moderator_msg['to'], '*****@*****.**') self.assertEqual( moderator_msg['subject'], 'New subscription request to Test from [email protected]') lines = moderator_msg.get_payload().splitlines() self.assertEqual(lines[-2].strip(), 'For: Bart Person <*****@*****.**>') self.assertEqual(lines[-1].strip(), 'List: [email protected]') # Now check the results message. self.assertEqual(str(results['subject']), 'The results of your email commands') self.assertMultiLineEqual( results.get_payload(), """\ The results of your email command are provided below. - Original message details: From: [email protected] Subject: Re: confirm {} Date: n/a Message-ID: n/a - Results: Confirmed - Done. """.format(token))
class TestSubscriptionModeration(unittest.TestCase): layer = RESTLayer maxDiff = None def setUp(self): with transaction(): self._mlist = create_list('*****@*****.**') self._registrar = IRegistrar(self._mlist) manager = getUtility(IUserManager) self._anne = manager.create_address( '*****@*****.**', 'Anne Person') self._bart = manager.make_user( '*****@*****.**', 'Bart Person') set_preferred(self._bart) def test_no_such_list(self): # Try to get the requests of a nonexistent list. with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com/' 'requests') self.assertEqual(cm.exception.code, 404) def test_no_such_subscription_token(self): # Bad request when the token is not in the database. with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com/' 'requests/missing') self.assertEqual(cm.exception.code, 404) def test_bad_subscription_action(self): # POSTing to a held message with a bad action. token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Let's try to handle her request, but with a bogus action. url = 'http://*****:*****@example.com/requests/{}' with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict( action='bogus', )) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.msg, b'Cannot convert parameters: action') def test_list_held_requests(self): # We can view all the held requests. with transaction(): token_1, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNotNone(token_1) self.assertIsNone(member) token_2, token_owner, member = self._registrar.register(self._bart) self.assertIsNotNone(token_2) self.assertIsNone(member) content, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(response.status, 200) self.assertEqual(content['total_size'], 2) tokens = set(json['token'] for json in content['entries']) self.assertEqual(tokens, {token_1, token_2}) emails = set(json['email'] for json in content['entries']) self.assertEqual(emails, {'*****@*****.**', '*****@*****.**'}) def test_individual_request(self): # We can view an individual request. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNotNone(token) self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' content, response = call_api(url.format(token)) self.assertEqual(response.status, 200) self.assertEqual(content['token'], token) self.assertEqual(content['token_owner'], token_owner.name) self.assertEqual(content['email'], '*****@*****.**') def test_accept(self): # POST to the request to accept it. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' content, response = call_api(url.format(token), dict( action='accept', )) self.assertEqual(response.status, 204) # Anne is a member. self.assertEqual( self._mlist.members.get_member('*****@*****.**').address, self._anne) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict( action='accept', )) self.assertEqual(cm.exception.code, 404) def test_accept_already_subscribed(self): # POST to a subscription request, but the user is already subscribed. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Make Anne already a member. self._mlist.subscribe(self._anne) # Accept the pending subscription, which raises an error. url = 'http://*****:*****@example.com' '/requests/bogus', dict(action='accept')) self.assertEqual(cm.exception.code, 404) def test_accept_by_moderator_clears_request_queue(self): # After accepting a message held for moderator approval, there are no # more requests to handle. # # We start with nothing in the queue. content, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(content['total_size'], 0) # Anne tries to subscribe to a list that only requests moderator # approval. with transaction(): self._mlist.subscription_policy = SubscriptionPolicy.moderate token, token_owner, member = self._registrar.register( self._anne, pre_verified=True, pre_confirmed=True) # There's now one request in the queue, and it's waiting on moderator # approval. content, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(content['total_size'], 1) json = content['entries'][0] self.assertEqual(json['token_owner'], 'moderator') self.assertEqual(json['email'], '*****@*****.**') # The moderator approves the request. url = 'http://*****:*****@example.com/requests/{}' content, response = call_api(url.format(token), {'action': 'accept'}) self.assertEqual(response.status, 204) # And now the request queue is empty. content, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(content['total_size'], 0) def test_discard(self): # POST to the request to discard it. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' content, response = call_api(url.format(token), dict( action='discard', )) self.assertEqual(response.status, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict( action='discard', )) self.assertEqual(cm.exception.code, 404) def test_defer(self): # Defer the decision for some other moderator. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' content, response = call_api(url.format(token), dict( action='defer', )) self.assertEqual(response.status, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL still exists. content, response = call_api(url.format(token), dict( action='defer', )) self.assertEqual(response.status, 204) # And now we can accept it. content, response = call_api(url.format(token), dict( action='accept', )) self.assertEqual(response.status, 204) # Anne is a member. self.assertEqual( self._mlist.members.get_member('*****@*****.**').address, self._anne) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict( action='accept', )) self.assertEqual(cm.exception.code, 404) def test_defer_bad_token(self): # Try to accept a request with a bogus token. with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com' '/requests/bogus', dict(action='defer')) self.assertEqual(cm.exception.code, 404) def test_reject(self): # POST to the request to reject it. This leaves a bounce message in # the virgin queue. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Clear out the virgin queue, which currently contains the # confirmation message sent to Anne. get_queue_messages('virgin') url = 'http://*****:*****@example.com/requests/{}' content, response = call_api(url.format(token), dict( action='reject', )) self.assertEqual(response.status, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict( action='reject', )) self.assertEqual(cm.exception.code, 404) # And the rejection message to Anne is now in the virgin queue. items = get_queue_messages('virgin') self.assertEqual(len(items), 1) message = items[0].msg self.assertEqual(message['From'], '*****@*****.**') self.assertEqual(message['To'], '*****@*****.**') self.assertEqual(message['Subject'], 'Request to mailing list "Ant" rejected') def test_reject_bad_token(self): # Try to accept a request with a bogus token. with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com' '/requests/bogus', dict(action='reject')) self.assertEqual(cm.exception.code, 404) def test_hold_keeps_holding(self): # POST to the request to continue holding it. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Clear out the virgin queue, which currently contains the # confirmation message sent to Anne. get_queue_messages('virgin') url = 'http://*****:*****@example.com/requests/{}' content, response = call_api(url.format(token), dict( action='hold', )) self.assertEqual(response.status, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL still exists. content, response = call_api(url.format(token), dict( action='defer', )) self.assertEqual(response.status, 204) def test_subscribe_other_role_with_no_preferred_address(self): with transaction(): cate = getUtility(IUserManager).create_user('*****@*****.**') with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com') with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com', 'role': 'moderator', }) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, b'Membership is banned')
class TestRegistrar(unittest.TestCase): """Test registration.""" layer = ConfigLayer def setUp(self): self._mlist = create_list('*****@*****.**') self._registrar = IRegistrar(self._mlist) self._pendings = getUtility(IPendings) self._anne = getUtility(IUserManager).create_address( '*****@*****.**') def test_initial_conditions(self): # Registering a subscription request provides a unique token associated # with a pendable, and the owner of the token. self.assertEqual(self._pendings.count, 0) token, token_owner, member = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(member) self.assertEqual(self._pendings.count, 1) record = self._pendings.confirm(token, expunge=False) self.assertEqual(record['list_id'], self._mlist.list_id) self.assertEqual(record['email'], '*****@*****.**') def test_subscribe(self): # Registering a subscription request where no confirmation or # moderation steps are needed, leaves us with no token or owner, since # there's nothing more to do. self._mlist.subscription_policy = SubscriptionPolicy.open self._anne.verified_on = now() token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) # There's nothing to confirm. record = self._pendings.confirm(token, expunge=False) self.assertIsNone(record) def test_no_such_token(self): # Given a token which is not in the database, a LookupError is raised. self._registrar.register(self._anne) self.assertRaises(LookupError, self._registrar.confirm, 'not-a-token') def test_confirm_because_verify(self): # We have a subscription request which requires the user to confirm # (because she does not have a verified address), but not the moderator # to approve. Running the workflow gives us a token. Confirming the # token subscribes the user. self._mlist.subscription_policy = SubscriptionPolicy.open token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription. token, token_owner, rmember = self._registrar.confirm(token) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_confirm_because_confirm(self): # We have a subscription request which requires the user to confirm # (because of list policy), but not the moderator to approve. Running # the workflow gives us a token. Confirming the token subscribes the # user. self._mlist.subscription_policy = SubscriptionPolicy.confirm self._anne.verified_on = now() token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription. token, token_owner, rmember = self._registrar.confirm(token) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_confirm_because_moderation(self): # We have a subscription request which requires the moderator to # approve. Running the workflow gives us a token. Confirming the # token subscribes the user. self._mlist.subscription_policy = SubscriptionPolicy.moderate self._anne.verified_on = now() token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.moderator) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription. token, token_owner, rmember = self._registrar.confirm(token) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_confirm_because_confirm_then_moderation(self): # We have a subscription request which requires the user to confirm # (because she does not have a verified address) and the moderator to # approve. Running the workflow gives us a token. Confirming the # token runs the workflow a little farther, but still gives us a # token. Confirming again subscribes the user. self._mlist.subscription_policy = \ SubscriptionPolicy.confirm_then_moderate self._anne.verified_on = now() # Runs until subscription confirmation. token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription, and wait for the moderator to approve # the subscription. She is still not subscribed. new_token, token_owner, rmember = self._registrar.confirm(token) # The new token, used for the moderator to approve the message, is not # the same as the old token. self.assertNotEqual(new_token, token) self.assertIsNotNone(new_token) self.assertEqual(token_owner, TokenOwner.moderator) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Confirm once more, this time as the moderator approving the # subscription. Now she's a member. token, token_owner, rmember = self._registrar.confirm(new_token) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_confirm_then_moderate_with_different_tokens(self): # Ensure that the confirmation token the user sees when they have to # confirm their subscription is different than the token the moderator # sees when they approve the subscription. This prevents the user # from using a replay attack to subvert moderator approval. self._mlist.subscription_policy = \ SubscriptionPolicy.confirm_then_moderate self._anne.verified_on = now() # Runs until subscription confirmation. token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription, and wait for the moderator to approve # the subscription. She is still not subscribed. new_token, token_owner, rmember = self._registrar.confirm(token) # The status is not true because the user has not yet been subscribed # to the mailing list. self.assertIsNotNone(new_token) self.assertEqual(token_owner, TokenOwner.moderator) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # The new token is different than the old token. self.assertNotEqual(token, new_token) # Trying to confirm with the old token does not work. self.assertRaises(LookupError, self._registrar.confirm, token) # Confirm once more, this time with the new token, as the moderator # approving the subscription. Now she's a member. done_token, token_owner, rmember = self._registrar.confirm(new_token) # The token is None, signifying that the member has been subscribed. self.assertIsNone(done_token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_discard_waiting_for_confirmation(self): # While waiting for a user to confirm their subscription, we discard # the workflow. self._mlist.subscription_policy = SubscriptionPolicy.confirm self._anne.verified_on = now() # Runs until subscription confirmation. token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now discard the subscription request. self._registrar.discard(token) # Trying to confirm the token now results in an exception. self.assertRaises(LookupError, self._registrar.confirm, token) def test_admin_notify_mchanges(self): # When a user gets subscribed via the subscription policy workflow, # the list administrators get an email notification. self._mlist.subscription_policy = SubscriptionPolicy.open self._mlist.admin_notify_mchanges = True self._mlist.send_welcome_message = False token, token_owner, member = self._registrar.register( self._anne, pre_verified=True) # Anne is now a member. self.assertEqual(member.address.email, '*****@*****.**') # And there's a notification email waiting for Bart. items = get_queue_messages('virgin') self.assertEqual(len(items), 1) message = items[0].msg self.assertEqual(message['To'], '*****@*****.**') self.assertEqual(message['Subject'], 'Ant subscription notification') self.assertEqual(message.get_payload(), """\ [email protected] has been successfully subscribed to Ant.""") def test_no_admin_notify_mchanges(self): # Even when a user gets subscribed via the subscription policy # workflow, the list administrators won't get an email notification if # they don't want one. self._mlist.subscription_policy = SubscriptionPolicy.open self._mlist.admin_notify_mchanges = False self._mlist.send_welcome_message = False # Bart is an administrator of the mailing list. bart = getUtility(IUserManager).create_address( '*****@*****.**', 'Bart Person') self._mlist.subscribe(bart, MemberRole.owner) token, token_owner, member = self._registrar.register( self._anne, pre_verified=True) # Anne is now a member. self.assertEqual(member.address.email, '*****@*****.**') # There's no notification email waiting for Bart. items = get_queue_messages('virgin') self.assertEqual(len(items), 0)
def on_post(self, request, response): """Create a new member.""" try: validator = Validator(list_id=str, subscriber=subscriber_validator, display_name=str, delivery_mode=enum_validator(DeliveryMode), role=enum_validator(MemberRole), pre_verified=bool, pre_confirmed=bool, pre_approved=bool, _optional=('delivery_mode', 'display_name', 'role', 'pre_verified', 'pre_confirmed', 'pre_approved')) arguments = validator(request) except ValueError as error: bad_request(response, str(error)) return # Dig the mailing list out of the arguments. list_id = arguments.pop('list_id') mlist = getUtility(IListManager).get_by_list_id(list_id) if mlist is None: bad_request(response, b'No such list') return # Figure out what kind of subscriber is being registered. Either it's # a user via their preferred email address or it's an explicit address. # If it's a UUID, then it must be associated with an existing user. subscriber = arguments.pop('subscriber') user_manager = getUtility(IUserManager) # We use the display name if there is one. display_name = arguments.pop('display_name', '') if isinstance(subscriber, UUID): user = user_manager.get_user_by_id(subscriber) if user is None: bad_request(response, b'No such user') return subscriber = user else: # This must be an email address. See if there's an existing # address object associated with this email. address = user_manager.get_address(subscriber) if address is None: # Create a new address, which of course will not be validated. address = user_manager.create_address(subscriber, display_name) subscriber = address # What role are we subscribing? Regular members go through the # subscription policy workflow while owners, moderators, and # nonmembers go through the legacy API for now. role = arguments.pop('role', MemberRole.member) if role is MemberRole.member: # Get the pre_ flags for the subscription workflow. pre_verified = arguments.pop('pre_verified', False) pre_confirmed = arguments.pop('pre_confirmed', False) pre_approved = arguments.pop('pre_approved', False) # Now we can run the registration process until either the # subscriber is subscribed, or the workflow is paused for # verification, confirmation, or approval. registrar = IRegistrar(mlist) try: token, token_owner, member = registrar.register( subscriber, pre_verified=pre_verified, pre_confirmed=pre_confirmed, pre_approved=pre_approved) except AlreadySubscribedError: conflict(response, b'Member already subscribed') return if token is None: assert token_owner is TokenOwner.no_one, token_owner # The subscription completed. Let's get the resulting member # and return the location to the new member. Member ids are # UUIDs and need to be converted to URLs because JSON doesn't # directly support UUIDs. member_id = member.member_id.int location = self.path_to('members/{0}'.format(member_id)) created(response, location) return # The member could not be directly subscribed because there are # some out-of-band steps that need to be completed. E.g. the user # must confirm their subscription or the moderator must approve # it. In this case, an HTTP 202 Accepted is exactly the code that # we should use, and we'll return both the confirmation token and # the "token owner" so the client knows who should confirm it. assert token is not None, token assert token_owner is not TokenOwner.no_one, token_owner assert member is None, member content = dict(token=token, token_owner=token_owner.name) accepted(response, etag(content)) return # 2015-04-15 BAW: We're subscribing some role other than a regular # member. Use the legacy API for this for now. assert role in (MemberRole.owner, MemberRole.moderator, MemberRole.nonmember) # 2015-04-15 BAW: We're limited to using an email address with this # legacy API, so if the subscriber is a user, the user must have a # preferred address, which we'll use, even though it will subscribe # the explicit address. It is an error if the user does not have a # preferred address. # # If the subscriber is an address object, just use that. if IUser.providedBy(subscriber): if subscriber.preferred_address is None: bad_request(response, b'User without preferred address') return email = subscriber.preferred_address.email else: assert IAddress.providedBy(subscriber) email = subscriber.email delivery_mode = arguments.pop('delivery_mode', DeliveryMode.regular) record = RequestRecord(email, display_name, delivery_mode) try: member = add_member(mlist, record, role) except InvalidEmailAddressError: bad_request(response, b'Invalid email address') return except MembershipIsBannedError: bad_request(response, b'Membership is banned') return # The subscription completed. Let's get the resulting member # and return the location to the new member. Member ids are # UUIDs and need to be converted to URLs because JSON doesn't # directly support UUIDs. member_id = member.member_id.int location = self.path_to('members/{0}'.format(member_id)) created(response, location)