diff options
| -rw-r--r-- | src/mailman/rest/docs/domains.rst | 40 | ||||
| -rw-r--r-- | src/mailman/rest/domains.py | 13 | ||||
| -rw-r--r-- | src/mailman/rest/listconf.py | 13 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_domains.py | 44 | ||||
| -rw-r--r-- | src/mailman/rest/users.py | 49 | ||||
| -rw-r--r-- | src/mailman/rest/validator.py | 9 |
6 files changed, 131 insertions, 37 deletions
diff --git a/src/mailman/rest/docs/domains.rst b/src/mailman/rest/docs/domains.rst index f57dee572..34e3b9a18 100644 --- a/src/mailman/rest/docs/domains.rst +++ b/src/mailman/rest/docs/domains.rst @@ -224,17 +224,9 @@ you can add some domain owners. Currently our domain has no owners: Anne and Bart volunteer to be a domain owners. :: - >>> dump_json('http://localhost:9001/3.0/domains/my.example.com/owners', { - ... 'owner': 'anne@example.com', - ... }) - content-length: 0 - date: ... - server: ... - status: 204 - - >>> dump_json('http://localhost:9001/3.0/domains/my.example.com/owners', { - ... 'owner': 'bart@example.com', - ... }) + >>> dump_json('http://localhost:9001/3.0/domains/my.example.com/owners', ( + ... ('owner', 'anne@example.com'), ('owner', 'bart@example.com') + ... )) content-length: 0 date: ... server: ... @@ -261,8 +253,34 @@ We can delete all the domain owners. >>> dump_json('http://localhost:9001/3.0/domains/my.example.com/owners', ... method='DELETE') + content-length: 0 + date: ... + server: ... + status: 204 + +Now there are no owners. >>> dump_json('http://localhost:9001/3.0/domains/my.example.com/owners') + http_etag: ... + start: 0 + total_size: 0 + +New domains can be created with owners. + + >>> dump_json('http://localhost:9001/3.0/domains', ( + ... ('mail_host', 'your.example.com'), + ... ('owner', 'anne@example.com'), + ... ('owner', 'bart@example.com'), + ... )) + content-length: 0 + date: ... + location: http://localhost:9001/3.0/domains/your.example.com + server: ... + status: 201 + +The new domain has the expected owners. + + >>> dump_json('http://localhost:9001/3.0/domains/your.example.com/owners') entry 0: created_on: 2005-08-01T07:49:23 http_etag: ... diff --git a/src/mailman/rest/domains.py b/src/mailman/rest/domains.py index dff2f2073..bf6fc5ca5 100644 --- a/src/mailman/rest/domains.py +++ b/src/mailman/rest/domains.py @@ -30,7 +30,7 @@ from mailman.rest.helpers import ( no_content, not_found, okay, path_to) from mailman.rest.lists import ListsForDomain from mailman.rest.users import OwnersForDomain -from mailman.rest.validator import Validator +from mailman.rest.validator import Validator, list_of_strings_validator from zope.component import getUtility @@ -110,10 +110,15 @@ class AllDomains(_DomainBase): validator = Validator(mail_host=str, description=str, base_url=str, - owners=list, - _optional=('description', 'base_url', - 'owners')) + owner=list_of_strings_validator, + _optional=( + 'description', 'base_url', 'owner')) values = validator(request) + # For consistency, owners are passed in as multiple `owner` keys, + # but .add() requires an `owners` keyword. Match impedence. + owners = values.pop('owner', None) + if owners is not None: + values['owners'] = owners domain = domain_manager.add(**values) except BadDomainSpecificationError as error: bad_request(response, str(error)) diff --git a/src/mailman/rest/listconf.py b/src/mailman/rest/listconf.py index e83f52833..d618ce116 100644 --- a/src/mailman/rest/listconf.py +++ b/src/mailman/rest/listconf.py @@ -32,7 +32,8 @@ from mailman.interfaces.autorespond import ResponseAction from mailman.interfaces.mailinglist import IAcceptableAliasSet, ReplyToMunging from mailman.rest.helpers import ( GetterSetter, bad_request, etag, no_content, okay) -from mailman.rest.validator import PatchValidator, Validator, enum_validator +from mailman.rest.validator import ( + PatchValidator, Validator, enum_validator, list_of_strings_validator) @@ -72,14 +73,6 @@ def pipeline_validator(pipeline_name): raise ValueError('Unknown pipeline: {}'.format(pipeline_name)) -def list_of_str(values): - """Turn a list of things into a list of unicodes.""" - for value in values: - if not isinstance(value, str): - raise ValueError('Expected str, got {!r}'.format(value)) - return values - - # This is the list of IMailingList attributes that are exposed through the # REST API. The values of the keys are the GetterSetter instance holding the @@ -96,7 +89,7 @@ def list_of_str(values): # (e.g. datetimes, timedeltas, enums). ATTRIBUTES = dict( - acceptable_aliases=AcceptableAliases(list_of_str), + acceptable_aliases=AcceptableAliases(list_of_strings_validator), admin_immed_notify=GetterSetter(as_boolean), admin_notify_mchanges=GetterSetter(as_boolean), administrivia=GetterSetter(as_boolean), diff --git a/src/mailman/rest/tests/test_domains.py b/src/mailman/rest/tests/test_domains.py index 13299516c..f87ceb3cc 100644 --- a/src/mailman/rest/tests/test_domains.py +++ b/src/mailman/rest/tests/test_domains.py @@ -18,6 +18,7 @@ """REST domain tests.""" __all__ = [ + 'TestDomainOwners', 'TestDomains', ] @@ -27,7 +28,6 @@ import unittest from mailman.app.lifecycle import create_list from mailman.database.transaction import transaction from mailman.interfaces.listmanager import IListManager -from mailman.interfaces.domain import IDomainManager from mailman.testing.helpers import call_api from mailman.testing.layers import RESTLayer from urllib.error import HTTPError @@ -99,3 +99,45 @@ class TestDomains(unittest.TestCase): call_api('http://localhost:9001/3.0/domains/example.com', method='DELETE') self.assertEqual(cm.exception.code, 404) + + + +class TestDomainOwners(unittest.TestCase): + layer = RESTLayer + + def test_get_missing_domain_owners(self): + # Try to get the owners of a missing domain. + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/domains/example.net/owners') + self.assertEqual(cm.exception.code, 404) + + def test_post_to_missing_domain_owners(self): + # Try to add owners to a missing domain. + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/domains/example.net/owners', ( + ('owner', 'dave@example.com'), ('owner', 'elle@example.com'), + )) + self.assertEqual(cm.exception.code, 404) + + def test_delete_missing_domain_owners(self): + # Try to delete the owners of a missing domain. + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/domains/example.net/owners', + method='DELETE') + self.assertEqual(cm.exception.code, 404) + + def test_bad_post(self): + # Send POST data with an invalid attribute. + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/domains/example.com/owners', ( + ('guy', 'dave@example.com'), ('gal', 'elle@example.com'), + )) + self.assertEqual(cm.exception.code, 400) + + def test_bad_delete(self): + # Send DELETE with any data. + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/domains/example.com/owners', { + 'owner': 'dave@example.com', + }, method='DELETE') + self.assertEqual(cm.exception.code, 400) diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py index a38299d00..7b1ec8040 100644 --- a/src/mailman/rest/users.py +++ b/src/mailman/rest/users.py @@ -38,7 +38,8 @@ from mailman.rest.helpers import ( conflict, created, etag, forbidden, no_content, not_found, okay, paginate, path_to) from mailman.rest.preferences import Preferences -from mailman.rest.validator import PatchValidator, Validator +from mailman.rest.validator import ( + PatchValidator, Validator, list_of_strings_validator) from passlib.utils import generate_password as generate from uuid import UUID from zope.component import getUtility @@ -48,14 +49,27 @@ from zope.component import getUtility # Attributes of a user which can be changed via the REST API. class PasswordEncrypterGetterSetter(GetterSetter): def __init__(self): - super(PasswordEncrypterGetterSetter, self).__init__( - config.password_context.encrypt) + super().__init__(config.password_context.encrypt) def get(self, obj, attribute): assert attribute == 'cleartext_password' - super(PasswordEncrypterGetterSetter, self).get(obj, 'password') + super().get(obj, 'password') def put(self, obj, attribute, value): assert attribute == 'cleartext_password' - super(PasswordEncrypterGetterSetter, self).put(obj, 'password', value) + super().put(obj, 'password', value) + + +class ListOfDomainOwners(GetterSetter): + def get(self, domain, attribute): + assert attribute == 'owner', ( + 'Unexpected attribute: {}'.format(attribute)) + def sort_key(owner): + return owner.addresses[0].email + return sorted(domain.owners, key=sort_key) + + def put(self, domain, attribute, value): + assert attribute == 'owner', ( + 'Unexpected attribute: {}'.format(attribute)) + domain.add_owners(value) ATTRIBUTES = dict( @@ -396,29 +410,42 @@ class OwnersForDomain(_UserBase): def on_get(self, request, response): """/domains/<domain>/owners""" + if self._domain is None: + not_found(response) + return resource = self._make_collection(request) okay(response, etag(resource)) def on_post(self, request, response): """POST to /domains/<domain>/owners """ - validator = Validator(owner=GetterSetter(str)) + if self._domain is None: + not_found(response) + return + validator = Validator( + owner=ListOfDomainOwners(list_of_strings_validator)) try: - values = validator(request) + validator.update(self._domain, request) except ValueError as error: bad_request(response, str(error)) return - self._domain.add_owner(values['owner']) return no_content(response) def on_delete(self, request, response): """DELETE to /domains/<domain>/owners""" - validator = Validator(owner=GetterSetter(str)) + if self._domain is None: + not_found(response) try: - values = validator(request) + # No arguments. + Validator()(request) except ValueError as error: bad_request(response, str(error)) return - self._domain.remove_owner(owner) + owner_email = [ + owner.addresses[0].email + for owner in self._domain.owners + ] + for email in owner_email: + self._domain.remove_owner(email) return no_content(response) @paginate diff --git a/src/mailman/rest/validator.py b/src/mailman/rest/validator.py index 867991a36..d09886e36 100644 --- a/src/mailman/rest/validator.py +++ b/src/mailman/rest/validator.py @@ -22,6 +22,7 @@ __all__ = [ 'Validator', 'enum_validator', 'language_validator', + 'list_of_strings_validator', 'subscriber_validator', ] @@ -66,6 +67,14 @@ def language_validator(code): return getUtility(ILanguageManager)[code] +def list_of_strings_validator(values): + """Turn a list of things into a list of unicodes.""" + for value in values: + if not isinstance(value, str): + raise ValueError('Expected str, got {!r}'.format(value)) + return values + + class Validator: """A validator of parameter input.""" |
