diff options
| -rw-r--r-- | src/mailman/app/tests/test_subscriptions.py | 20 | ||||
| -rw-r--r-- | src/mailman/model/member.py | 2 | ||||
| -rw-r--r-- | src/mailman/rest/docs/membership.rst | 41 | ||||
| -rw-r--r-- | src/mailman/rest/docs/sub-moderation.rst | 47 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_membership.py | 50 |
5 files changed, 131 insertions, 29 deletions
diff --git a/src/mailman/app/tests/test_subscriptions.py b/src/mailman/app/tests/test_subscriptions.py index 096b7dac6..3cfde47bc 100644 --- a/src/mailman/app/tests/test_subscriptions.py +++ b/src/mailman/app/tests/test_subscriptions.py @@ -545,6 +545,26 @@ approval: # The address is now verified. self.assertIsNotNone(anne.verified_on) + def test_do_confirm_verify_user(self): + # A confirmation step is necessary when a user subscribes with their + # preferred address, and we are not pre-confirming. + anne = self._user_manager.create_user(self._anne) + address = list(anne.addresses)[0] + address.verified_on = now() + anne.preferred_address = address + # Run the workflow to model the confirmation step. There is no + # subscriber attribute yet. + workflow = SubscriptionWorkflow(self._mlist, anne) + list(workflow) + self.assertEqual(workflow.subscriber, anne) + # Do a confirmation workflow, which should now set the subscriber. + confirm_workflow = SubscriptionWorkflow(self._mlist) + confirm_workflow.token = workflow.token + confirm_workflow.restore() + confirm_workflow.run_thru('do_confirm_verify') + # The address is now verified. + self.assertEqual(confirm_workflow.subscriber, anne) + def test_do_confirmation_subscribes_user(self): # Subscriptions to the mailing list must be confirmed. Once that's # done, the user's address (which is not initially verified) gets diff --git a/src/mailman/model/member.py b/src/mailman/model/member.py index d1bbf3796..77d7935a4 100644 --- a/src/mailman/model/member.py +++ b/src/mailman/model/member.py @@ -122,7 +122,7 @@ class Member(Model): if new_address.verified_on is None: # A member cannot change their subscription address to an # unverified address. - raise UnverifiedAddressError(new_address) + raise UnverifiedAddressError('Unverified address') user = getUtility(IUserManager).get_user(new_address.email) if user is None or user != self.user: raise MembershipError('Address is not controlled by user') diff --git a/src/mailman/rest/docs/membership.rst b/src/mailman/rest/docs/membership.rst index 20444f969..0b00a8808 100644 --- a/src/mailman/rest/docs/membership.rst +++ b/src/mailman/rest/docs/membership.rst @@ -634,9 +634,14 @@ A user can be subscribed to a mailing list via the REST API, either by a specific address, or more generally by their preferred address. A subscribed user is called a member. -The list owner wants to subscribe Elly to the `ant` mailing list. Since -Elly's email address is not yet known to Mailman, a user is created for her. -By default, get gets a regular delivery. +Elly subscribes to the `ant` mailing list. Since her email address is not yet +known to Mailman, a user is created for her. By default, she gets a regular +delivery. + +By pre-verifying her subscription, we don't require Elly to verify that her +email address is valid. By pre-confirming her subscription too, no +confirmation email will be sent. Pre-approval means that the list moderator +won't have to approve her subscription request. >>> dump_json('http://localhost:9001/3.0/members', { ... 'list_id': 'ant.example.com', @@ -903,6 +908,36 @@ his membership ids have not changed. start: 0 total_size: 2 +When changing his subscription address, Herb may also decide to change his +mode of delivery. +:: + + >>> dump_json('http://localhost:9001/3.0/members/11', { + ... 'address': 'herb@example.com', + ... 'delivery_mode': 'mime_digests', + ... }, method='PATCH') + content-length: 0 + date: ... + server: ... + status: 204 + + >>> dump_json('http://localhost:9001/3.0/addresses/' + ... 'herb@example.com/memberships') + entry 0: + address: http://localhost:9001/3.0/addresses/herb@example.com + delivery_mode: mime_digests + email: herb@example.com + http_etag: "..." + list_id: bee.example.com + member_id: 11 + moderation_action: defer + role: member + self_link: http://localhost:9001/3.0/members/11 + user: http://localhost:9001/3.0/users/7 + http_etag: "..." + start: 0 + total_size: 1 + Moderating a member =================== diff --git a/src/mailman/rest/docs/sub-moderation.rst b/src/mailman/rest/docs/sub-moderation.rst index 79cee6fb7..92c0c8849 100644 --- a/src/mailman/rest/docs/sub-moderation.rst +++ b/src/mailman/rest/docs/sub-moderation.rst @@ -16,40 +16,37 @@ A mailing list starts with no pending subscription or unsubscription requests. >>> from mailman.interfaces.mailinglist import SubscriptionPolicy >>> ant.subscription_policy = SubscriptionPolicy.moderate >>> transaction.commit() - >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/requests') + >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com/requests') http_etag: "..." start: 0 total_size: 0 When Anne tries to subscribe to the Ant list, her subscription is held for -moderator approval. +moderator approval. Her email address is pre-verified and her subscription +request is pre-confirmed, but because the mailing list is moderated, a token +is returned to track her subscription request. - >>> from mailman.interfaces.registrar import IRegistrar - >>> from mailman.interfaces.usermanager import IUserManager - >>> from zope.component import getUtility - >>> registrar = IRegistrar(ant) - >>> manager = getUtility(IUserManager) - >>> anne = manager.create_address('anne@example.com', 'Anne Person') - >>> token, token_owner, member = registrar.register( - ... anne, pre_verified=True, pre_confirmed=True) - >>> print(member) - None - -The message is being held for moderator approval. - - >>> print(token_owner.name) - moderator + >>> dump_json('http://localhost:9001/3.0/members', { + ... 'list_id': 'ant.example.com', + ... 'subscriber': 'anne@example.com', + ... 'display_name': 'Anne Person', + ... 'pre_verified': True, + ... 'pre_confirmed': True, + ... }) + http_etag: ... + token: 0000000000000000000000000000000000000001 + token_owner: moderator The subscription request can be viewed in the REST API. >>> transaction.commit() - >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/requests') + >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com/requests') entry 0: display_name: Anne Person email: anne@example.com http_etag: "..." list_id: ant.example.com - token: ... + token: 0000000000000000000000000000000000000001 token_owner: moderator type: subscription when: 2005-08-01T07:49:23 @@ -64,13 +61,13 @@ Viewing individual requests You can view an individual membership change request by providing the token (a.k.a. request id). Anne's subscription request looks like this. - >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/' - ... 'requests/{}'.format(token)) + >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com/' + ... 'requests/0000000000000000000000000000000000000001') display_name: Anne Person email: anne@example.com http_etag: "..." list_id: ant.example.com - token: ... + token: 0000000000000000000000000000000000000001 token_owner: moderator type: subscription when: 2005-08-01T07:49:23 @@ -90,8 +87,8 @@ request's resource. The POST data requires an action of one of the following: Anne's subscription request is accepted. - >>> dump_json('http://localhost:9001/3.0/lists/' - ... 'ant@example.com/requests/{}'.format(token), + >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com/requests' + ... '/0000000000000000000000000000000000000001', ... {'action': 'accept'}) content-length: 0 date: ... @@ -106,7 +103,7 @@ Anne is now a member of the mailing list. There are no more membership change requests. - >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/requests') + >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com/requests') http_etag: "..." start: 0 total_size: 0 diff --git a/src/mailman/rest/tests/test_membership.py b/src/mailman/rest/tests/test_membership.py index 4dbf70024..a7f54703a 100644 --- a/src/mailman/rest/tests/test_membership.py +++ b/src/mailman/rest/tests/test_membership.py @@ -124,6 +124,18 @@ class TestMembership(unittest.TestCase): self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, b'User has no preferred address') + def test_subscribe_bogus_user_by_uid(self): + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.1/members', { + 'list_id': 'test.example.com', + 'subscriber': '00000000000000000000000000000801', + 'pre_verified': True, + 'pre_confirmed': True, + 'pre_approved': True, + }) + self.assertEqual(cm.exception.code, 400) + self.assertEqual(cm.exception.reason, b'No such user') + def test_add_member_with_mixed_case_email(self): # LP: #1425359 - Mailman is case-perserving, case-insensitive. This # test subscribes the lower case address and ensures the original mixed @@ -251,6 +263,44 @@ class TestMembership(unittest.TestCase): {}, method='PATCH') self.assertEqual(cm.exception.code, 404) + def test_patch_membership_with_bogus_address(self): + # Try to change a subscription address to one that does not yet exist. + with transaction(): + subscribe(self._mlist, 'Anne') + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/members/1', { + 'address': 'bogus@example.com', + }, method='PATCH') + self.assertEqual(cm.exception.code, 400) + self.assertEqual(cm.exception.reason, b'Address not registered') + + def test_patch_membership_with_unverified_address(self): + # Try to change a subscription address to one that is not yet verified. + with transaction(): + subscribe(self._mlist, 'Anne') + self._usermanager.create_address('anne.person@example.com') + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/members/1', { + 'address': 'anne.person@example.com', + }, method='PATCH') + self.assertEqual(cm.exception.code, 400) + self.assertEqual(cm.exception.reason, b'Unverified address') + + def test_patch_membership_of_preferred_address(self): + # Try to change a subscription to an address when the user is + # subscribed via their preferred address. + with transaction(): + subscribe(self._mlist, 'Anne') + anne = self._usermanager.create_address('anne.person@example.com') + anne.verified_on = now() + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/members/1', { + 'address': 'anne.person@example.com', + }, method='PATCH') + self.assertEqual(cm.exception.code, 400) + self.assertEqual(cm.exception.reason, + b'Address is not controlled by user') + def test_patch_member_bogus_attribute(self): # /members/<id> PATCH 'bogus' returns 400 with transaction(): |
