diff options
| author | Barry Warsaw | 2011-04-20 04:51:25 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2011-04-20 04:51:25 -0400 |
| commit | f9dfad95995685c18adf509630e1f6307ea332a1 (patch) | |
| tree | 446ad657c2b1d2e7ba6fe41689fd1efb18752bb0 /src | |
| parent | 6b4a3ebc37e5e11d74161fed12b3cf75eca6c339 (diff) | |
| download | mailman-f9dfad95995685c18adf509630e1f6307ea332a1.tar.gz mailman-f9dfad95995685c18adf509630e1f6307ea332a1.tar.zst mailman-f9dfad95995685c18adf509630e1f6307ea332a1.zip | |
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/model/tests/test_user.py | 1 | ||||
| -rw-r--r-- | src/mailman/rest/docs/membership.txt | 78 | ||||
| -rw-r--r-- | src/mailman/rest/docs/users.txt | 6 | ||||
| -rw-r--r-- | src/mailman/rest/lists.py | 2 | ||||
| -rw-r--r-- | src/mailman/rest/members.py | 7 | ||||
| -rw-r--r-- | src/mailman/rest/tests/__init__.py | 0 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_membership.py | 162 | ||||
| -rw-r--r-- | src/mailman/testing/helpers.py | 53 | ||||
| -rw-r--r-- | src/mailman/tests/test_documentation.py | 32 |
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): |
