From 3a306dec00019225cb6ebb8415077657bb2022b1 Mon Sep 17 00:00:00 2001 From: Florian Fuchs Date: Sun, 13 Apr 2014 14:48:20 -0400 Subject: * Added an API endpoint to POST new email addresses to a user resource. * Updated docs. --- src/mailman/rest/addresses.py | 18 ++++++++++++++++ src/mailman/rest/docs/addresses.rst | 34 +++++++++++++++++++++++++++++++ src/mailman/rest/tests/test_addresses.py | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/src/mailman/rest/addresses.py b/src/mailman/rest/addresses.py index 51bbe2046..3949944b1 100644 --- a/src/mailman/rest/addresses.py +++ b/src/mailman/rest/addresses.py @@ -34,6 +34,7 @@ from zope.component import getUtility from mailman.rest.helpers import CollectionMixin, etag, no_content, path_to from mailman.rest.members import MemberCollection from mailman.rest.preferences import Preferences +from mailman.interfaces.address import ExistingAddressError from mailman.interfaces.usermanager import IUserManager from mailman.utilities.datetime import now @@ -174,6 +175,23 @@ class UserAddresses(_AddressBase): resource = self._make_collection(request) return http.ok([], etag(resource)) + @resource.POST() + def create(self, request): + """Add a new address to the user record.""" + email = request.POST.get('email') + if not email: + return http.bad_request([], b'No email address provided.') + # Create a new address and connect it to the current user + try: + address = getUtility(IUserManager).create_address(email) + address.user = self._user + location = path_to('addresses/{0}'.format(address.email)) + return http.created(location, [], None) + # ... except the address already exists. + except ExistingAddressError: + return http.bad_request( + [], b'Address already exists: {0}'.format(email)) + def membership_key(member): diff --git a/src/mailman/rest/docs/addresses.rst b/src/mailman/rest/docs/addresses.rst index f05b6b9b2..a12d2cbbc 100644 --- a/src/mailman/rest/docs/addresses.rst +++ b/src/mailman/rest/docs/addresses.rst @@ -174,6 +174,40 @@ addresses live in the /addresses namespace. self_link: http://localhost:9001/3.0/addresses/dave@example.com +A user record can have multiple email addresses associated with it. +Adding a new address can be done by posting to the /addresses namespace. + + >>> dump_json('http://localhost:9001/3.0/users/dave@example.com/' + ... 'addresses', { + ... 'email': 'dave.person@example.org' + ... }) + content-length: 0 + date: ... + location: http://localhost:9001/3.0/addresses/dave.person@example.org + server: ... + status: 201 + +The new address now shows up in the result of the user's /addresses endpoint. + + >>> dump_json('http://localhost:9001/3.0/users/dave@example.com/addresses') + entry 0: + email: dave.person@example.org + http_etag: "..." + original_email: dave.person@example.org + registered_on: 2005-08-01T07:49:23 + self_link: http://localhost:9001/3.0/addresses/dave.person@example.org + entry 1: + display_name: Dave Person + email: dave@example.com + http_etag: "..." + original_email: dave@example.com + registered_on: 2005-08-01T07:49:23 + self_link: http://localhost:9001/3.0/addresses/dave@example.com + http_etag: "..." + start: 0 + total_size: 2 + + Memberships =========== diff --git a/src/mailman/rest/tests/test_addresses.py b/src/mailman/rest/tests/test_addresses.py index 9d9e44c22..7b89d79de 100644 --- a/src/mailman/rest/tests/test_addresses.py +++ b/src/mailman/rest/tests/test_addresses.py @@ -109,3 +109,38 @@ class TestAddresses(unittest.TestCase): call_api('http://localhost:9001/3.0/addresses/' 'anne@example.com/unverify/foo', {}) self.assertEqual(cm.exception.code, 400) + + def test_address_added_to_user(self): + # Address is added to a user record. + with transaction(): + anne = getUtility(IUserManager).create_user('anne@example.com') + response, content = call_api('http://localhost:9001/3.0/users/' + 'anne@example.com/addresses', { + 'email': 'anne.person@example.org' + }) + self.assertTrue('anne.person@example.org' in [a.email for a in + anne.addresses]) + self.assertEqual(content['status'], '201') + + def test_existing_address_bad_request(self): + # Posting an existing address returns 400. + with transaction(): + anne = getUtility(IUserManager).create_user('anne@example.com') + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/users/' + 'anne@example.com/addresses', { + 'email': 'anne@example.com' + }) + self.assertEqual(cm.exception.code, 400) + self.assertEqual(cm.exception.reason, 'Address already exists: ' + 'anne@example.com') + + def test_empty_address_bad_request(self): + # Posting no address returns 400. + with transaction(): + anne = getUtility(IUserManager).create_user('anne@example.com') + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/users/' + 'anne@example.com/addresses', {}) + self.assertEqual(cm.exception.code, 400) + self.assertEqual(cm.exception.reason, 'No email address provided.') -- cgit v1.3.1 From e4a141e068d9c4132f8b143dc49faabe095a95c8 Mon Sep 17 00:00:00 2001 From: Florian Fuchs Date: Mon, 14 Apr 2014 13:47:58 -0400 Subject: Instantiating an Address model now validates the email string --- src/mailman/model/address.py | 5 +++- src/mailman/model/tests/test_address.py | 45 +++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/mailman/model/tests/test_address.py diff --git a/src/mailman/model/address.py b/src/mailman/model/address.py index f5e7ddbfd..890047445 100644 --- a/src/mailman/model/address.py +++ b/src/mailman/model/address.py @@ -27,11 +27,13 @@ __all__ = [ from email.utils import formataddr from storm.locals import DateTime, Int, Reference, Unicode +from zope.component import getUtility from zope.event import notify from zope.interface import implementer from mailman.database.model import Model -from mailman.interfaces.address import AddressVerificationEvent, IAddress +from mailman.interfaces.address import (AddressVerificationEvent, IAddress, + IEmailValidator) from mailman.utilities.datetime import now @@ -54,6 +56,7 @@ class Address(Model): def __init__(self, email, display_name): super(Address, self).__init__() + getUtility(IEmailValidator).validate(email) lower_case = email.lower() self.email = lower_case self.display_name = display_name diff --git a/src/mailman/model/tests/test_address.py b/src/mailman/model/tests/test_address.py new file mode 100644 index 000000000..f436bacf7 --- /dev/null +++ b/src/mailman/model/tests/test_address.py @@ -0,0 +1,45 @@ +# Copyright (C) 2011-2014 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 . + +"""Test addresses.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'TestAddress', + ] + + +import unittest + +from zope.component import getUtility + +from mailman.email.validate import InvalidEmailAddressError +from mailman.model.address import Address +from mailman.testing.layers import ConfigLayer + + + +class TestAddress(unittest.TestCase): + """Test addresses.""" + + layer = ConfigLayer + + def test_invalid_email_string_raises_exception(self): + with self.assertRaises(InvalidEmailAddressError): + Address('not_a_valid_email_string', '') -- cgit v1.3.1 From 5b3b837431f24fd7afdbd368d6d6b7989d1fd54a Mon Sep 17 00:00:00 2001 From: Florian Fuchs Date: Mon, 14 Apr 2014 14:04:42 -0400 Subject: Posting an invalid email address string to the user/addresses endpoint now returns a 400 status code --- src/mailman/rest/addresses.py | 7 +++++-- src/mailman/rest/tests/test_addresses.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/mailman/rest/addresses.py b/src/mailman/rest/addresses.py index 3949944b1..4001b007e 100644 --- a/src/mailman/rest/addresses.py +++ b/src/mailman/rest/addresses.py @@ -34,7 +34,7 @@ from zope.component import getUtility from mailman.rest.helpers import CollectionMixin, etag, no_content, path_to from mailman.rest.members import MemberCollection from mailman.rest.preferences import Preferences -from mailman.interfaces.address import ExistingAddressError +from mailman.interfaces.address import ExistingAddressError, InvalidEmailAddressError from mailman.interfaces.usermanager import IUserManager from mailman.utilities.datetime import now @@ -187,7 +187,10 @@ class UserAddresses(_AddressBase): address.user = self._user location = path_to('addresses/{0}'.format(address.email)) return http.created(location, [], None) - # ... except the address already exists. + # ... except the address is not valid ... + except InvalidEmailAddressError: + return http.bad_request([], b'Invalid email address') + # ... or the address already exists. except ExistingAddressError: return http.bad_request( [], b'Address already exists: {0}'.format(email)) diff --git a/src/mailman/rest/tests/test_addresses.py b/src/mailman/rest/tests/test_addresses.py index 7b89d79de..ded29d056 100644 --- a/src/mailman/rest/tests/test_addresses.py +++ b/src/mailman/rest/tests/test_addresses.py @@ -135,6 +135,18 @@ class TestAddresses(unittest.TestCase): self.assertEqual(cm.exception.reason, 'Address already exists: ' 'anne@example.com') + def test_invalid_address_bad_request(self): + # Posting an invalid address string returns 400. + with transaction(): + anne = getUtility(IUserManager).create_user('anne@example.com') + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/users/' + 'anne@example.com/addresses', { + 'email': 'invalid_address_string' + }) + self.assertEqual(cm.exception.code, 400) + self.assertEqual(cm.exception.reason, 'Invalid email address') + def test_empty_address_bad_request(self): # Posting no address returns 400. with transaction(): -- cgit v1.3.1