summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/app/tests/test_subscriptions.py20
-rw-r--r--src/mailman/model/member.py2
-rw-r--r--src/mailman/rest/docs/membership.rst41
-rw-r--r--src/mailman/rest/docs/sub-moderation.rst47
-rw-r--r--src/mailman/rest/tests/test_membership.py50
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():