# Copyright (C) 2012-2016 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 moderation tests."""
__all__ = [
'TestPostModeration',
'TestSubscriptionModeration',
]
import unittest
from mailman.app.lifecycle import create_list
from mailman.app.moderator import hold_message
from mailman.database.transaction import transaction
from mailman.interfaces.mailinglist import SubscriptionPolicy
from mailman.interfaces.registrar import IRegistrar
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import (
call_api, get_queue_messages, specialized_message_from_string as mfs)
from mailman.testing.layers import RESTLayer
from mailman.utilities.datetime import now
from urllib.error import HTTPError
from zope.component import getUtility
class TestPostModeration(unittest.TestCase):
layer = RESTLayer
def setUp(self):
with transaction():
self._mlist = create_list('ant@example.com')
self._msg = mfs("""\
From: anne@example.com
To: ant@example.com
Subject: Something
Message-ID:
Something else.
""")
def test_not_found(self):
# When a bogus mailing list is given, 404 should result.
with self.assertRaises(HTTPError) as cm:
call_api('http://localhost:9001/3.0/lists/bee@example.com/held')
self.assertEqual(cm.exception.code, 404)
def test_bad_held_message_request_id(self):
# Bad request when request_id is not an integer.
with self.assertRaises(HTTPError) as cm:
call_api(
'http://localhost:9001/3.0/lists/ant@example.com/held/bogus')
self.assertEqual(cm.exception.code, 400)
def test_missing_held_message_request_id(self):
# Not found when the request_id is not in the database.
with self.assertRaises(HTTPError) as cm:
call_api('http://localhost:9001/3.0/lists/ant@example.com/held/99')
self.assertEqual(cm.exception.code, 404)
def test_bad_held_message_action(self):
# POSTing to a held message with a bad action.
held_id = hold_message(self._mlist, self._msg)
url = 'http://localhost:9001/3.0/lists/ant@example.com/held/{0}'
with self.assertRaises(HTTPError) as cm:
call_api(url.format(held_id), {'action': 'bogus'})
self.assertEqual(cm.exception.code, 400)
self.assertEqual(cm.exception.msg,
b'Cannot convert parameters: action')
def test_discard(self):
# Discarding a message removes it from the moderation queue.
with transaction():
held_id = hold_message(self._mlist, self._msg)
url = 'http://localhost:9001/3.0/lists/ant@example.com/held/{}'.format(
held_id)
content, response = call_api(url, dict(action='discard'))
self.assertEqual(response.status, 204)
# Now it's gone.
with self.assertRaises(HTTPError) as cm:
call_api(url, dict(action='discard'))
self.assertEqual(cm.exception.code, 404)
class TestSubscriptionModeration(unittest.TestCase):
layer = RESTLayer
maxDiff = None
def setUp(self):
with transaction():
self._mlist = create_list('ant@example.com')
self._registrar = IRegistrar(self._mlist)
manager = getUtility(IUserManager)
self._anne = manager.create_address(
'anne@example.com', 'Anne Person')
self._bart = manager.make_user(
'bart@example.com', 'Bart Person')
preferred = list(self._bart.addresses)[0]
preferred.verified_on = now()
self._bart.preferred_address = preferred
def test_no_such_list(self):
# Try to get the requests of a nonexistent list.
with self.assertRaises(HTTPError) as cm:
call_api('http://localhost:9001/3.0/lists/bee@example.com/'
'requests')
self.assertEqual(cm.exception.code, 404)
def test_no_such_subscription_token(self):
# Bad request when the token is not in the database.
with self.assertRaises(HTTPError) as cm:
call_api('http://localhost:9001/3.0/lists/ant@example.com/'
'requests/missing')
self.assertEqual(cm.exception.code, 404)
def test_bad_subscription_action(self):
# POSTing to a held message with a bad action.
token, token_owner, member = self._registrar.register(self._anne)
# Anne's subscription request got held.
self.assertIsNone(member)
# Let's try to handle her request, but with a bogus action.
url = 'http://localhost:9001/3.0/lists/ant@example.com/requests/{}'
with self.assertRaises(HTTPError) as cm:
call_api(url.format(token), dict(
action='bogus',
))
self.assertEqual(cm.exception.code, 400)
self.assertEqual(cm.exception.msg,
b'Cannot convert parameters: action')
def test_list_held_requests(self):
# We can view all the held requests.
with transaction():
token_1, token_owner, member = self._registrar.register(self._anne)
# Anne's subscription request got held.
self.assertIsNotNone(token_1)
self.assertIsNone(member)
token_2, token_owner, member = self._registrar.register(self._bart)
self.assertIsNotNone(token_2)
self.assertIsNone(member)
content, response = call_api(
'http://localhost:9001/3.0/lists/ant@example.com/requests')
self.assertEqual(response.status, 200)
self.assertEqual(content['total_size'], 2)
tokens = set(json['token'] for json in content['entries'])
self.assertEqual(tokens, {token_1, token_2})
emails = set(json['email'] for json in content['entries'])
self.assertEqual(emails, {'anne@example.com', 'bart@example.com'})
def test_individual_request(self):
# We can view an individual request.
with transaction():
token, token_owner, member = self._registrar.register(self._anne)
# Anne's subscription request got held.
self.assertIsNotNone(token)
self.assertIsNone(member)
url = 'http://localhost:9001/3.0/lists/ant@example.com/requests/{}'
content, response = call_api(url.format(token))
self.assertEqual(response.status, 200)
self.assertEqual(content['token'], token)
self.assertEqual(content['token_owner'], token_owner.name)
self.assertEqual(content['email'], 'anne@example.com')
def test_accept(self):
# POST to the request to accept it.
with transaction():
token, token_owner, member = self._registrar.register(self._anne)
# Anne's subscription request got held.
self.assertIsNone(member)
url = 'http://localhost:9001/3.0/lists/ant@example.com/requests/{}'
content, response = call_api(url.format(token), dict(
action='accept',
))
self.assertEqual(response.status, 204)
# Anne is a member.
self.assertEqual(
self._mlist.members.get_member('anne@example.com').address,
self._anne)
# The request URL no longer exists.
with self.assertRaises(HTTPError) as cm:
call_api(url.format(token), dict(
action='accept',
))
self.assertEqual(cm.exception.code, 404)
def test_accept_bad_token(self):
# Try to accept a request with a bogus token.
with self.assertRaises(HTTPError) as cm:
call_api('http://localhost:9001/3.0/lists/ant@example.com'
'/requests/bogus',
dict(action='accept'))
self.assertEqual(cm.exception.code, 404)
def test_accept_by_moderator_clears_request_queue(self):
# After accepting a message held for moderator approval, there are no
# more requests to handle.
#
# We start with nothing in the queue.
content, response = call_api(
'http://localhost:9001/3.0/lists/ant@example.com/requests')
self.assertEqual(content['total_size'], 0)
# Anne tries to subscribe to a list that only requests moderator
# approval.
with transaction():
self._mlist.subscription_policy = SubscriptionPolicy.moderate
token, token_owner, member = self._registrar.register(
self._anne,
pre_verified=True, pre_confirmed=True)
# There's now one request in the queue, and it's waiting on moderator
# approval.
content, response = call_api(
'http://localhost:9001/3.0/lists/ant@example.com/requests')
self.assertEqual(content['total_size'], 1)
json = content['entries'][0]
self.assertEqual(json['token_owner'], 'moderator')
self.assertEqual(json['email'], 'anne@example.com')
# The moderator approves the request.
url = 'http://localhost:9001/3.0/lists/ant@example.com/requests/{}'
content, response = call_api(url.format(token), {'action': 'accept'})
self.assertEqual(response.status, 204)
# And now the request queue is empty.
content, response = call_api(
'http://localhost:9001/3.0/lists/ant@example.com/requests')
self.assertEqual(content['total_size'], 0)
def test_discard(self):
# POST to the request to discard it.
with transaction():
token, token_owner, member = self._registrar.register(self._anne)
# Anne's subscription request got held.
self.assertIsNone(member)
url = 'http://localhost:9001/3.0/lists/ant@example.com/requests/{}'
content, response = call_api(url.format(token), dict(
action='discard',
))
self.assertEqual(response.status, 204)
# Anne is not a member.
self.assertIsNone(self._mlist.members.get_member('anne@example.com'))
# The request URL no longer exists.
with self.assertRaises(HTTPError) as cm:
call_api(url.format(token), dict(
action='discard',
))
self.assertEqual(cm.exception.code, 404)
def test_defer(self):
# Defer the decision for some other moderator.
with transaction():
token, token_owner, member = self._registrar.register(self._anne)
# Anne's subscription request got held.
self.assertIsNone(member)
url = 'http://localhost:9001/3.0/lists/ant@example.com/requests/{}'
content, response = call_api(url.format(token), dict(
action='defer',
))
self.assertEqual(response.status, 204)
# Anne is not a member.
self.assertIsNone(self._mlist.members.get_member('anne@example.com'))
# The request URL still exists.
content, response = call_api(url.format(token), dict(
action='defer',
))
self.assertEqual(response.status, 204)
# And now we can accept it.
content, response = call_api(url.format(token), dict(
action='accept',
))
self.assertEqual(response.status, 204)
# Anne is a member.
self.assertEqual(
self._mlist.members.get_member('anne@example.com').address,
self._anne)
# The request URL no longer exists.
with self.assertRaises(HTTPError) as cm:
call_api(url.format(token), dict(
action='accept',
))
self.assertEqual(cm.exception.code, 404)
def test_defer_bad_token(self):
# Try to accept a request with a bogus token.
with self.assertRaises(HTTPError) as cm:
call_api('http://localhost:9001/3.0/lists/ant@example.com'
'/requests/bogus',
dict(action='defer'))
self.assertEqual(cm.exception.code, 404)
def test_reject(self):
# POST to the request to reject it. This leaves a bounce message in
# the virgin queue.
with transaction():
token, token_owner, member = self._registrar.register(self._anne)
# Anne's subscription request got held.
self.assertIsNone(member)
# Clear out the virgin queue, which currently contains the
# confirmation message sent to Anne.
get_queue_messages('virgin')
url = 'http://localhost:9001/3.0/lists/ant@example.com/requests/{}'
content, response = call_api(url.format(token), dict(
action='reject',
))
self.assertEqual(response.status, 204)
# Anne is not a member.
self.assertIsNone(self._mlist.members.get_member('anne@example.com'))
# The request URL no longer exists.
with self.assertRaises(HTTPError) as cm:
call_api(url.format(token), dict(
action='reject',
))
self.assertEqual(cm.exception.code, 404)
# And the rejection message to Anne is now in the virgin queue.
items = get_queue_messages('virgin')
self.assertEqual(len(items), 1)
message = items[0].msg
self.assertEqual(message['From'], 'ant-bounces@example.com')
self.assertEqual(message['To'], 'anne@example.com')
self.assertEqual(message['Subject'],
'Request to mailing list "Ant" rejected')
def test_reject_bad_token(self):
# Try to accept a request with a bogus token.
with self.assertRaises(HTTPError) as cm:
call_api('http://localhost:9001/3.0/lists/ant@example.com'
'/requests/bogus',
dict(action='reject'))
self.assertEqual(cm.exception.code, 404)