summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2011-04-20 04:51:25 -0400
committerBarry Warsaw2011-04-20 04:51:25 -0400
commitf9dfad95995685c18adf509630e1f6307ea332a1 (patch)
tree446ad657c2b1d2e7ba6fe41689fd1efb18752bb0 /src
parent6b4a3ebc37e5e11d74161fed12b3cf75eca6c339 (diff)
downloadmailman-f9dfad95995685c18adf509630e1f6307ea332a1.tar.gz
mailman-f9dfad95995685c18adf509630e1f6307ea332a1.tar.zst
mailman-f9dfad95995685c18adf509630e1f6307ea332a1.zip
Diffstat (limited to 'src')
-rw-r--r--src/mailman/model/tests/test_user.py1
-rw-r--r--src/mailman/rest/docs/membership.txt78
-rw-r--r--src/mailman/rest/docs/users.txt6
-rw-r--r--src/mailman/rest/lists.py2
-rw-r--r--src/mailman/rest/members.py7
-rw-r--r--src/mailman/rest/tests/__init__.py0
-rw-r--r--src/mailman/rest/tests/test_membership.py162
-rw-r--r--src/mailman/testing/helpers.py53
-rw-r--r--src/mailman/tests/test_documentation.py32
9 files changed, 232 insertions, 109 deletions
diff --git a/src/mailman/model/tests/test_user.py b/src/mailman/model/tests/test_user.py
index 6bac37e41..072b2f7be 100644
--- a/src/mailman/model/tests/test_user.py
+++ b/src/mailman/model/tests/test_user.py
@@ -79,4 +79,3 @@ def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestUser))
return suite
-
diff --git a/src/mailman/rest/docs/membership.txt b/src/mailman/rest/docs/membership.txt
index 1e4e0414e..e4ab02ef7 100644
--- a/src/mailman/rest/docs/membership.txt
+++ b/src/mailman/rest/docs/membership.txt
@@ -293,81 +293,3 @@ Fred joins the alpha mailing list but wants MIME digest delivery.
>>> memberships[0]
<Member: Fred Person <fperson@example.com>
on alpha@example.com as MemberRole.member>
-
-
-Corner cases
-============
-
-For some reason Elly tries to join a mailing list that does not exist.
-
- >>> dump_json('http://localhost:9001/3.0/members', {
- ... 'fqdn_listname': 'beta@example.com',
- ... 'address': 'eperson@example.com',
- ... 'real_name': 'Elly Person',
- ... })
- Traceback (most recent call last):
- ...
- HTTPError: HTTP Error 400: No such list
-
-Then, she tries to leave a mailing list that does not exist.
-
- >>> dump_json('http://localhost:9001/3.0/lists/beta@example.com'
- ... '/members/eperson@example.com',
- ... method='DELETE')
- Traceback (most recent call last):
- ...
- HTTPError: HTTP Error 404: 404 Not Found
-
-She then tries to leave a mailing list with a bogus address.
-
- >>> dump_json('http://localhost:9001/3.0/lists/alpha@example.com'
- ... '/members/elly',
- ... method='DELETE')
- Traceback (most recent call last):
- ...
- HTTPError: HTTP Error 404: 404 Not Found
-
-For some reason, Elly tries to leave the mailing list again, but she's already
-been unsubscribed.
-
- >>> dump_json('http://localhost:9001/3.0/lists/alpha@example.com'
- ... '/members/eperson@example.com',
- ... method='DELETE')
- Traceback (most recent call last):
- ...
- HTTPError: HTTP Error 404: 404 Not Found
-
-Anna tries to join a mailing list she's already a member of.
-
- >>> dump_json('http://localhost:9001/3.0/members', {
- ... 'fqdn_listname': 'alpha@example.com',
- ... 'address': 'aperson@example.com',
- ... })
- Traceback (most recent call last):
- ...
- HTTPError: HTTP Error 409: Member already subscribed
-
-Gwen tries to join the alpha mailing list using an invalid delivery mode.
-
- >>> dump_json('http://localhost:9001/3.0/members', {
- ... 'fqdn_listname': 'alpha@example.com',
- ... 'address': 'gperson@example.com',
- ... 'real_name': 'Gwen Person',
- ... 'delivery_mode': 'in_digests',
- ... })
- Traceback (most recent call last):
- ...
- HTTPError: HTTP Error 400: Cannot convert parameters: delivery_mode
-
-Even using an address with "funny" characters Hugh can join the mailing list.
-
- >>> transaction.abort()
- >>> dump_json('http://localhost:9001/3.0/members', {
- ... 'fqdn_listname': 'alpha@example.com',
- ... 'address': 'hugh/person@example.com',
- ... 'real_name': 'Hugh Person',
- ... })
- content-length: 0
- date: ...
- location: http://localhost:9001/3.0/lists/alpha@example.com/member/hugh%2Fperson@example.com
- ...
diff --git a/src/mailman/rest/docs/users.txt b/src/mailman/rest/docs/users.txt
index 114ca4e49..49a2469e3 100644
--- a/src/mailman/rest/docs/users.txt
+++ b/src/mailman/rest/docs/users.txt
@@ -237,3 +237,9 @@ In fact, any of these addresses can be used to look up Bart's user record.
real_name: Bart Person
self_link: http://localhost:9001/3.0/users/3
user_id: 3
+
+
+Memberships
+===========
+
+Users subscribed to
diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py
index 686dd27f3..991f67e2f 100644
--- a/src/mailman/rest/lists.py
+++ b/src/mailman/rest/lists.py
@@ -146,6 +146,8 @@ class AList(_ListBase):
@resource.child(member_matcher)
def member(self, request, segments, role, address):
"""Return a single member representation."""
+ if self._mlist is None:
+ return http.not_found()
return AMember(self._mlist, role, address)
@resource.child(roster_matcher)
diff --git a/src/mailman/rest/members.py b/src/mailman/rest/members.py
index 3235f8738..0f74e20d7 100644
--- a/src/mailman/rest/members.py
+++ b/src/mailman/rest/members.py
@@ -36,7 +36,7 @@ from mailman.app.membership import delete_member
from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.listmanager import NoSuchListError
from mailman.interfaces.member import (
- AlreadySubscribedError, DeliveryMode, MemberRole)
+ AlreadySubscribedError, DeliveryMode, MemberRole, NotAMemberError)
from mailman.interfaces.membership import ISubscriptionService
from mailman.rest.helpers import CollectionMixin, etag, path_to
from mailman.rest.validator import Validator, enum_validator
@@ -83,7 +83,10 @@ class AMember(_MemberBase):
# owner. Handle the former case first. For now too, we will not send
# an admin or user notification.
if self._role is MemberRole.member:
- delete_member(self._mlist, self._address, False, False)
+ try:
+ delete_member(self._mlist, self._address, False, False)
+ except NotAMemberError:
+ return http.not_found()
else:
self._member.unsubscribe()
return http.ok([], '')
diff --git a/src/mailman/rest/tests/__init__.py b/src/mailman/rest/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/rest/tests/__init__.py
diff --git a/src/mailman/rest/tests/test_membership.py b/src/mailman/rest/tests/test_membership.py
new file mode 100644
index 000000000..933d1d368
--- /dev/null
+++ b/src/mailman/rest/tests/test_membership.py
@@ -0,0 +1,162 @@
+# Copyright (C) 2011 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""REST membership tests."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'test_suite',
+ ]
+
+
+import unittest
+
+from urllib2 import HTTPError
+from zope.component import getUtility
+
+from mailman.app.lifecycle import create_list
+from mailman.config import config
+from mailman.interfaces.usermanager import IUserManager
+from mailman.testing.helpers import call_api
+from mailman.testing.layers import RESTLayer
+
+
+
+class TestMembership(unittest.TestCase):
+ layer = RESTLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ config.db.commit()
+ self._usermanager = getUtility(IUserManager)
+
+ def test_try_to_join_missing_list(self):
+ # A user tries to join a non-existent list.
+ try:
+ # For Python 2.6.
+ call_api('http://localhost:9001/3.0/members', {
+ 'fqdn_listname': 'missing@example.com',
+ 'address': 'nobody@example.com',
+ })
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 400)
+ self.assertEqual(exc.msg, 'No such list')
+ else:
+ raise AssertionError('Expected HTTPError')
+
+ def test_try_to_leave_missing_list(self):
+ # A user tries to leave a non-existent list.
+ try:
+ # For Python 2.6.
+ call_api('http://localhost:9001/3.0/lists/missing@example.com'
+ '/member/nobody@example.com',
+ method='DELETE')
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 404)
+ self.assertEqual(exc.msg, '404 Not Found')
+ else:
+ raise AssertionError('Expected HTTPError')
+
+ def test_try_to_leave_list_with_bogus_address(self):
+ # Try to leave a mailing list using an invalid membership address.
+ try:
+ # For Python 2.6.
+ call_api('http://localhost:9001/3.0/lists/test@example.com'
+ '/member/nobody',
+ method='DELETE')
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 404)
+ self.assertEqual(exc.msg, '404 Not Found')
+ else:
+ raise AssertionError('Expected HTTPError')
+
+ def test_try_to_leave_a_list_twice(self):
+ anne = self._usermanager.create_address('anne@example.com')
+ self._mlist.subscribe(anne)
+ config.db.commit()
+ url = ('http://localhost:9001/3.0/lists/test@example.com'
+ '/member/anne@example.com')
+ content, response = call_api(url, method='DELETE')
+ # For a successful DELETE, the response code is 200 and there is no
+ # content.
+ self.assertEqual(content, None)
+ self.assertEqual(response.status, 200)
+ try:
+ # For Python 2.6.
+ call_api(url, method='DELETE')
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 404)
+ self.assertEqual(exc.msg, '404 Not Found')
+ else:
+ raise AssertionError('Expected HTTPError')
+
+ def test_try_to_join_a_list_twice(self):
+ anne = self._usermanager.create_address('anne@example.com')
+ self._mlist.subscribe(anne)
+ config.db.commit()
+ try:
+ # For Python 2.6.
+ call_api('http://localhost:9001/3.0/members', {
+ 'fqdn_listname': 'test@example.com',
+ 'address': 'anne@example.com',
+ })
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 409)
+ self.assertEqual(exc.msg, 'Member already subscribed')
+ else:
+ raise AssertionError('Expected HTTPError')
+
+ def test_join_with_invalid_delivery_mode(self):
+ try:
+ call_api('http://localhost:9001/3.0/members', {
+ 'fqdn_listname': 'test@example.com',
+ 'address': 'anne@example.com',
+ 'real_name': 'Anne Person',
+ 'delivery_mode': 'invalid-mode',
+ })
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 400)
+ self.assertEqual(exc.msg,
+ 'Cannot convert parameters: delivery_mode')
+ else:
+ raise AssertionError('Expected HTTPError')
+
+ def test_join_email_contains_slash(self):
+ content, response = call_api('http://localhost:9001/3.0/members', {
+ 'fqdn_listname': 'test@example.com',
+ 'address': 'hugh/person@example.com',
+ 'real_name': 'Hugh Person',
+ })
+ self.assertEqual(content, None)
+ self.assertEqual(response.status, 201)
+ self.assertEqual(response['location'],
+ 'http://localhost:9001/3.0/lists/test@example.com'
+ '/member/hugh%2Fperson@example.com')
+ # Reset any current transaction.
+ config.db.abort()
+ members = list(self._mlist.members.members)
+ self.assertEqual(len(members), 1)
+ self.assertEqual(members[0].address.email, 'hugh/person@example.com')
+
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(TestMembership))
+ return suite
diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py
index bd52d5d6e..b5df649e0 100644
--- a/src/mailman/testing/helpers.py
+++ b/src/mailman/testing/helpers.py
@@ -21,6 +21,7 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'TestableMaster',
+ 'call_api',
'digest_mbox',
'event_subscribers',
'get_lmtp_client',
@@ -33,6 +34,7 @@ __all__ = [
import os
+import json
import time
import errno
import signal
@@ -41,7 +43,11 @@ import smtplib
import datetime
import threading
+from base64 import b64encode
from contextlib import contextmanager
+from httplib2 import Http
+from urllib import urlencode
+from urllib2 import HTTPError
from zope import event
from zope.component import getUtility
@@ -243,6 +249,53 @@ def wait_for_webservice():
break
+def call_api(url, data=None, method=None, username=None, password=None):
+ """'Call a URL with a given HTTP method and return the resulting object.
+
+ The object will have been JSON decoded.
+
+ :param url: The url to open, read, and print.
+ :type url: string
+ :param data: Data to use to POST to a URL.
+ :type data: dict
+ :param method: Alternative HTTP method to use.
+ :type method: str
+ :param username: The HTTP Basic Auth user name. None means use the value
+ from the configuration.
+ :type username: str
+ :param password: The HTTP Basic Auth password. None means use the value
+ from the configuration.
+ :type username: str
+ :return: The response object and the JSON decoded content, if there is
+ any. If not, the second tuple item will be None.
+ :raises HTTPError: when a non-2xx return code is received.
+ """
+ headers = {}
+ if data is not None:
+ data = urlencode(data, doseq=True)
+ headers['Content-Type'] = 'application/x-www-form-urlencoded'
+ if method is None:
+ if data is None:
+ method = 'GET'
+ else:
+ method = 'POST'
+ method = method.upper()
+ basic_auth = '{0}:{1}'.format(
+ (config.webservice.admin_user if username is None else username),
+ (config.webservice.admin_pass if password is None else password))
+ headers['Authorization'] = 'Basic ' + b64encode(basic_auth)
+ response, content = Http().request(url, method, data, headers)
+ # If we did not get a 2xx status code, make this look like a urllib2
+ # exception, for backward compatibility with existing doctests.
+ if response.status // 100 != 2:
+ raise HTTPError(url, response.status, content, response, None)
+ if len(content) == 0:
+ return None, response
+ # XXX Workaround http://bugs.python.org/issue10038
+ content = unicode(content)
+ return json.loads(content), response
+
+
@contextmanager
def event_subscribers(*subscribers):
diff --git a/src/mailman/tests/test_documentation.py b/src/mailman/tests/test_documentation.py
index 2a72a367f..ee8298b7f 100644
--- a/src/mailman/tests/test_documentation.py
+++ b/src/mailman/tests/test_documentation.py
@@ -31,21 +31,17 @@ __all__ = [
import os
import sys
-import json
import doctest
import unittest
-from base64 import b64encode
from email import message_from_string
-from httplib2 import Http
-from urllib import urlencode
-from urllib2 import HTTPError
import mailman
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.email.message import Message
+from mailman.testing.helpers import call_api
from mailman.testing.layers import SMTPLayer
@@ -145,32 +141,12 @@ def call_http(url, data=None, method=None, username=None, password=None):
:return: The decoded JSON data structure.
:raises HTTPError: when a non-2xx return code is received.
"""
- headers = {}
- if data is not None:
- data = urlencode(data, doseq=True)
- headers['Content-Type'] = 'application/x-www-form-urlencoded'
- if method is None:
- if data is None:
- method = 'GET'
- else:
- method = 'POST'
- method = method.upper()
- basic_auth = '{0}:{1}'.format(
- (config.webservice.admin_user if username is None else username),
- (config.webservice.admin_pass if password is None else password))
- headers['Authorization'] = 'Basic ' + b64encode(basic_auth)
- response, content = Http().request(url, method, data, headers)
- # If we did not get a 2xx status code, make this look like a urllib2
- # exception, for backward compatibility with existing doctests.
- if response.status // 100 != 2:
- raise HTTPError(url, response.status, content, response, None)
- if len(content) == 0:
+ content, response = call_api(url, data, method, username, password)
+ if content is None:
for header in sorted(response):
print '{0}: {1}'.format(header, response[header])
return None
- # XXX Workaround http://bugs.python.org/issue10038
- content = unicode(content)
- return json.loads(content)
+ return content
def dump_json(url, data=None, method=None, username=None, password=None):