# Copyright (C) 2011-2017 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 . """REST membership tests.""" import unittest from mailman.app.lifecycle import create_list from mailman.config import config from mailman.database.transaction import transaction from mailman.interfaces.bans import IBanManager from mailman.interfaces.mailinglist import SubscriptionPolicy from mailman.interfaces.member import DeliveryMode, MemberRole from mailman.interfaces.subscriptions import ISubscriptionManager, TokenOwner from mailman.interfaces.usermanager import IUserManager from mailman.runners.incoming import IncomingRunner from mailman.testing.helpers import ( TestableMaster, call_api, get_lmtp_client, make_testable_runner, set_preferred, subscribe, wait_for_webservice) from mailman.testing.layers import ConfigLayer, RESTLayer from mailman.utilities.datetime import now from urllib.error import HTTPError from zope.component import getUtility class TestMembership(unittest.TestCase): layer = RESTLayer def setUp(self): with transaction(): self._mlist = create_list('test@example.com') self._usermanager = getUtility(IUserManager) def test_try_to_join_missing_list(self): # A user tries to join a non-existent list. with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members', { 'list_id': 'missing.example.com', 'subscriber': 'nobody@example.com', }) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, 'No such list') def test_try_to_leave_missing_list(self): # A user tries to leave a non-existent list. with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/lists/missing@example.com' '/member/nobody@example.com', method='DELETE') self.assertEqual(cm.exception.code, 404) def test_try_to_leave_list_with_bogus_address(self): # Try to leave a mailing list using an invalid membership address. with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members/1', method='DELETE') self.assertEqual(cm.exception.code, 404) def test_try_to_leave_a_list_twice(self): with transaction(): anne = self._usermanager.create_address('anne@example.com') self._mlist.subscribe(anne) url = 'http://localhost:9001/3.0/members/1' json, response = call_api(url, method='DELETE') # For a successful DELETE, the response code is 204 and there is no # content. self.assertEqual(json, None) self.assertEqual(response.status_code, 204) with self.assertRaises(HTTPError) as cm: call_api(url, method='DELETE') self.assertEqual(cm.exception.code, 404) def test_try_to_join_a_list_twice(self): with transaction(): anne = self._usermanager.create_address('anne@example.com') self._mlist.subscribe(anne) with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members', { 'list_id': 'test.example.com', 'subscriber': 'anne@example.com', 'pre_verified': True, 'pre_confirmed': True, 'pre_approved': True, }) self.assertEqual(cm.exception.code, 409) self.assertEqual(cm.exception.reason, 'Member already subscribed') def test_try_to_join_a_list_twice_issue260(self): with transaction(): anne = self._usermanager.create_address('anne@example.com') self._mlist.subscribe(anne) with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members', { 'list_id': 'test.example.com', 'subscriber': 'anne@example.com', 'pre_verified': False, 'pre_confirmed': False, 'pre_approved': False, }) self.assertEqual(cm.exception.code, 409) self.assertEqual(cm.exception.reason, 'Member already subscribed') def test_subscribe_user_without_preferred_address(self): with transaction(): getUtility(IUserManager).create_user('anne@example.com') # Subscribe the user to the mailing list by hex UUID. with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.1/members', { 'list_id': 'test.example.com', 'subscriber': '00000000000000000000000000000001', 'pre_verified': True, 'pre_confirmed': True, 'pre_approved': True, }) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, '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, '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 # case address can't be subscribed. with transaction(): anne = self._usermanager.create_address('anne@example.com') self._mlist.subscribe(anne) with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members', { 'list_id': 'test.example.com', 'subscriber': 'ANNE@example.com', 'pre_verified': True, 'pre_confirmed': True, 'pre_approved': True, }) self.assertEqual(cm.exception.code, 409) self.assertEqual(cm.exception.reason, 'Member already subscribed') def test_add_member_with_lower_case_email(self): # LP: #1425359 - Mailman is case-perserving, case-insensitive. This # test subscribes the mixed case address and ensures the lower cased # address can't be added. with transaction(): anne = self._usermanager.create_address('ANNE@example.com') self._mlist.subscribe(anne) with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members', { 'list_id': 'test.example.com', 'subscriber': 'anne@example.com', 'pre_verified': True, 'pre_confirmed': True, 'pre_approved': True, }) self.assertEqual(cm.exception.code, 409) self.assertEqual(cm.exception.reason, 'Member already subscribed') def test_join_with_invalid_delivery_mode(self): with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members', { 'list_id': 'test.example.com', 'subscriber': 'anne@example.com', 'display_name': 'Anne Person', 'delivery_mode': 'invalid-mode', }) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, 'Cannot convert parameters: delivery_mode') def test_join_email_contains_slash(self): json, response = call_api('http://localhost:9001/3.0/members', { 'list_id': 'test.example.com', 'subscriber': 'hugh/person@example.com', 'display_name': 'Hugh Person', 'pre_verified': True, 'pre_confirmed': True, 'pre_approved': True, }) self.assertEqual(json, None) self.assertEqual(response.status_code, 201) self.assertEqual(response.headers['location'], 'http://localhost:9001/3.0/members/1') # 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_join_as_user_with_preferred_address(self): with transaction(): anne = self._usermanager.create_user('anne@example.com') set_preferred(anne) self._mlist.subscribe(anne) json, response = call_api('http://localhost:9001/3.0/members') self.assertEqual(response.status_code, 200) self.assertEqual(int(json['total_size']), 1) entry_0 = json['entries'][0] self.assertEqual(entry_0['self_link'], 'http://localhost:9001/3.0/members/1') self.assertEqual(entry_0['role'], 'member') self.assertEqual(entry_0['user'], 'http://localhost:9001/3.0/users/1') self.assertEqual(entry_0['email'], 'anne@example.com') self.assertEqual( entry_0['address'], 'http://localhost:9001/3.0/addresses/anne@example.com') self.assertEqual(entry_0['list_id'], 'test.example.com') def test_duplicate_pending_subscription(self): # Issue #199 - a member's subscription is already pending and they try # to subscribe again. registrar = ISubscriptionManager(self._mlist) with transaction(): self._mlist.subscription_policy = SubscriptionPolicy.moderate anne = self._usermanager.create_address('anne@example.com') 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://localhost:9001/3.0/members', { 'list_id': 'test.example.com', 'subscriber': 'anne@example.com', 'pre_verified': True, 'pre_confirmed': True, }) self.assertEqual(cm.exception.code, 409) self.assertEqual(cm.exception.reason, 'Subscription request already pending') def test_duplicate_other_pending_subscription(self): # Issue #199 - a member's subscription is already pending and they try # to subscribe again. Unlike above, this pend is waiting for the user # to confirm their subscription. registrar = ISubscriptionManager(self._mlist) with transaction(): self._mlist.subscription_policy = ( SubscriptionPolicy.confirm_then_moderate) anne = self._usermanager.create_address('anne@example.com') token, token_owner, member = registrar.register( anne, pre_verified=True) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(member) with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members', { 'list_id': 'test.example.com', 'subscriber': 'anne@example.com', 'pre_verified': True, 'pre_confirmed': True, }) self.assertEqual(cm.exception.code, 409) self.assertEqual(cm.exception.reason, 'Subscription request already pending') def test_member_changes_preferred_address(self): with transaction(): anne = self._usermanager.create_user('anne@example.com') set_preferred(anne) self._mlist.subscribe(anne) # Take a look at Anne's current membership. json, response = call_api('http://localhost:9001/3.0/members') self.assertEqual(int(json['total_size']), 1) entry_0 = json['entries'][0] self.assertEqual(entry_0['email'], 'anne@example.com') self.assertEqual( entry_0['address'], 'http://localhost:9001/3.0/addresses/anne@example.com') # Anne registers a new address and makes it her preferred address. # There are no changes to her membership. with transaction(): new_preferred = anne.register('aperson@example.com') new_preferred.verified_on = now() anne.preferred_address = new_preferred # Take another look at Anne's current membership. json, response = call_api('http://localhost:9001/3.0/members') self.assertEqual(int(json['total_size']), 1) entry_0 = json['entries'][0] self.assertEqual(entry_0['email'], 'aperson@example.com') self.assertEqual( entry_0['address'], 'http://localhost:9001/3.0/addresses/aperson@example.com') def test_get_nonexistent_member(self): # /members/ returns 404 with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members/bogus') self.assertEqual(cm.exception.code, 404) def test_patch_nonexistent_member(self): # /members/ PATCH returns 404 with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members/801', {}, 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, '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, '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, 'Address is not controlled by user') def test_patch_member_bogus_attribute(self): # /members/ PATCH 'bogus' returns 400 with transaction(): anne = self._usermanager.create_address('anne@example.com') self._mlist.subscribe(anne) with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members/1', { 'powers': 'super', }, method='PATCH') self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, 'Unexpected parameters: powers') def test_member_all_without_preferences(self): # /members//all should return a 404 when it isn't trailed by # `preferences` with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members/1/all') self.assertEqual(cm.exception.code, 404) def test_patch_member_invalid_moderation_action(self): # /members/ PATCH with invalid 'moderation_action' returns 400. with transaction(): anne = self._usermanager.create_address('anne@example.com') self._mlist.subscribe(anne) with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members/1', { 'moderation_action': 'invalid', }, method='PATCH') self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, 'Cannot convert parameters: moderation_action') def test_bad_preferences_url(self): with transaction(): subscribe(self._mlist, 'Anne') with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members/1/preferences/bogus') self.assertEqual(cm.exception.code, 404) def test_not_a_member_preferences(self): with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members/1/preferences') self.assertEqual(cm.exception.code, 404) def test_not_a_member_all_preferences(self): with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members/1/all/preferences') self.assertEqual(cm.exception.code, 404) def test_delete_other_role(self): with transaction(): subscribe(self._mlist, 'Anne', MemberRole.moderator) json, response = call_api( 'http://localhost:9001/3.0/members/1', method='DELETE') self.assertEqual(response.status_code, 204) self.assertEqual(len(list(self._mlist.moderators.members)), 0) def test_banned_member_tries_to_join(self): # A user tries to join a list they are banned from. with transaction(): IBanManager(self._mlist).ban('anne@example.com') with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members', { 'list_id': 'test.example.com', 'subscriber': 'anne@example.com', }) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, 'Membership is banned') def test_globally_banned_member_tries_to_join(self): # A user tries to join a list they are banned from. with transaction(): IBanManager(None).ban('anne@example.com') with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/members', { 'list_id': 'test.example.com', 'subscriber': 'anne@example.com', }) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, 'Membership is banned') class CustomLayer(ConfigLayer): """Custom layer which starts both the REST and LMTP servers.""" server = None client = None @classmethod def _wait_for_both(cls): cls.client = get_lmtp_client(quiet=True) wait_for_webservice() @classmethod def setUp(cls): assert cls.server is None, 'Layer already set up' cls.server = TestableMaster(cls._wait_for_both) cls.server.start('lmtp', 'rest') @classmethod def tearDown(cls): assert cls.server is not None, 'Layer is not set up' cls.server.stop() cls.server = None class TestNonmembership(unittest.TestCase): layer = CustomLayer def setUp(self): with transaction(): self._mlist = create_list('test@example.com') self._usermanager = getUtility(IUserManager) def _go(self, message): lmtp = get_lmtp_client(quiet=True) lmtp.lhlo('remote.example.org') lmtp.sendmail('nonmember@example.com', ['test@example.com'], message) lmtp.close() # The message will now be sitting in the `in` queue. Run the incoming # runner once to process it, which should result in the nonmember # showing up. inq = make_testable_runner(IncomingRunner, 'in') inq.run() def test_nonmember_findable_after_posting(self): # A nonmember we have never seen before posts a message to the mailing # list. They are findable through the /members/find API using a role # of nonmember. self._go("""\ From: nonmember@example.com To: test@example.com Subject: Nonmember post Message-ID: Some text. """) # Now use the REST API to try to find the nonmember. json, response = call_api( 'http://localhost:9001/3.0/members/find', { # 'list_id': 'test.example.com', 'role': 'nonmember', }) self.assertEqual(json['total_size'], 1) nonmember = json['entries'][0] self.assertEqual(nonmember['role'], 'nonmember') self.assertEqual(nonmember['email'], 'nonmember@example.com') self.assertEqual( nonmember['address'], 'http://localhost:9001/3.0/addresses/nonmember@example.com') # There is no user key in the JSON data because there is no user # record associated with the address record. self.assertNotIn('user', nonmember) def test_linked_nonmember_findable_after_posting(self): # Like above, a nonmember posts a message to the mailing list. In # this case though, the nonmember already has a user record. They are # findable through the /members/find API using a role of nonmember. with transaction(): self._usermanager.create_user('nonmember@example.com') self._go("""\ From: nonmember@example.com To: test@example.com Subject: Nonmember post Message-ID: Some text. """) # Now use the REST API to try to find the nonmember. json, response = call_api( 'http://localhost:9001/3.0/members/find', { # 'list_id': 'test.example.com', 'role': 'nonmember', }) self.assertEqual(json['total_size'], 1) nonmember = json['entries'][0] self.assertEqual(nonmember['role'], 'nonmember') self.assertEqual(nonmember['email'], 'nonmember@example.com') self.assertEqual( nonmember['address'], 'http://localhost:9001/3.0/addresses/nonmember@example.com') # There is a user key in the JSON data because the address had # previously been linked to a user record. self.assertEqual(nonmember['user'], 'http://localhost:9001/3.0/users/1') class TestAPI31Members(unittest.TestCase): layer = RESTLayer def setUp(self): with transaction(): self._mlist = create_list('ant@example.com') def test_member_ids_are_hex(self): with transaction(): subscribe(self._mlist, 'Anne') subscribe(self._mlist, 'Bart') json, response = call_api('http://localhost:9001/3.1/members') entries = json['entries'] self.assertEqual(len(entries), 2) self.assertEqual( entries[0]['self_link'], 'http://localhost:9001/3.1/members/00000000000000000000000000000001') self.assertEqual( entries[0]['member_id'], '00000000000000000000000000000001') self.assertEqual( entries[0]['user'], 'http://localhost:9001/3.1/users/00000000000000000000000000000001') self.assertEqual( entries[1]['self_link'], 'http://localhost:9001/3.1/members/00000000000000000000000000000002') self.assertEqual( entries[1]['member_id'], '00000000000000000000000000000002') self.assertEqual( entries[1]['user'], 'http://localhost:9001/3.1/users/00000000000000000000000000000002') def test_get_member_id_by_hex(self): with transaction(): subscribe(self._mlist, 'Anne') json, response = call_api( 'http://localhost:9001/3.1/members/00000000000000000000000000000001') self.assertEqual( json['member_id'], '00000000000000000000000000000001') self.assertEqual( json['self_link'], 'http://localhost:9001/3.1/members/00000000000000000000000000000001') self.assertEqual( json['user'], 'http://localhost:9001/3.1/users/00000000000000000000000000000001') self.assertEqual( json['address'], 'http://localhost:9001/3.1/addresses/aperson@example.com') def test_get_list_member_id_by_email(self): with transaction(): subscribe(self._mlist, 'Anne', email="aperson@example.com") json, response = call_api( 'http://localhost:9001/3.1/lists/ant.example.com/member' '/aperson@example.com') self.assertEqual( json['member_id'], '00000000000000000000000000000001') self.assertEqual( json['self_link'], 'http://localhost:9001/3.1/members/00000000000000000000000000000001') self.assertEqual( json['user'], 'http://localhost:9001/3.1/users/00000000000000000000000000000001') self.assertEqual( json['address'], 'http://localhost:9001/3.1/addresses/aperson@example.com') def test_cannot_get_member_id_by_int(self): with transaction(): subscribe(self._mlist, 'Anne') with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.1/members/1') self.assertEqual(cm.exception.code, 404) def test_preferences(self): with transaction(): member = subscribe(self._mlist, 'Anne') member.preferences.delivery_mode = DeliveryMode.summary_digests json, response = call_api( 'http://localhost:9001/3.1/members' '/00000000000000000000000000000001/preferences') self.assertEqual(json['delivery_mode'], 'summary_digests') def test_all_preferences(self): with transaction(): member = subscribe(self._mlist, 'Anne') member.preferences.delivery_mode = DeliveryMode.summary_digests json, response = call_api( 'http://localhost:9001/3.1/members' '/00000000000000000000000000000001/all/preferences') self.assertEqual(json['delivery_mode'], 'summary_digests') def test_create_new_membership_by_hex(self): with transaction(): user = getUtility(IUserManager).create_user('anne@example.com') set_preferred(user) # Subscribe the user to the mailing list by hex UUID. json, response = call_api( 'http://localhost:9001/3.1/members', { 'list_id': 'ant.example.com', 'subscriber': '00000000000000000000000000000001', 'pre_verified': True, 'pre_confirmed': True, 'pre_approved': True, }) self.assertEqual(response.status_code, 201) self.assertEqual( response.headers['location'], 'http://localhost:9001/3.1/members/00000000000000000000000000000001' ) def test_create_new_owner_by_hex(self): with transaction(): user = getUtility(IUserManager).create_user('anne@example.com') set_preferred(user) # Subscribe the user to the mailing list by hex UUID. json, response = call_api( 'http://localhost:9001/3.1/members', { 'list_id': 'ant.example.com', 'subscriber': '00000000000000000000000000000001', 'role': 'owner', }) self.assertEqual(response.status_code, 201) self.assertEqual( response.headers['location'], 'http://localhost:9001/3.1/members/00000000000000000000000000000001' ) def test_cannot_create_new_membership_by_int(self): with transaction(): user = getUtility(IUserManager).create_user('anne@example.com') set_preferred(user) # We can't use the int representation of the UUID with API 3.1. with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.1/members', { 'list_id': 'ant.example.com', 'subscriber': '1', 'pre_verified': True, 'pre_confirmed': True, 'pre_approved': True, }) # This is a bad request because the `subscriber` value isn't something # that's known to the system, in API 3.1. It's not technically a 404 # because that's reserved for URL lookups. self.assertEqual(cm.exception.code, 400) def test_duplicate_owner(self): # Server failure when someone is already an owner. with transaction(): anne = getUtility(IUserManager).create_address('anne@example.com') self._mlist.subscribe(anne, MemberRole.owner) with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.1/members', { 'list_id': 'ant.example.com', 'subscriber': anne.email, 'role': 'owner', }) self.assertEqual(cm.exception.code, 400) self.assertEqual( cm.exception.reason, 'anne@example.com is already an owner of ant@example.com')