diff options
Diffstat (limited to 'src/mailman/rest')
| -rw-r--r-- | src/mailman/rest/docs/lists.rst | 11 | ||||
| -rw-r--r-- | src/mailman/rest/docs/plugins.rst | 60 | ||||
| -rw-r--r-- | src/mailman/rest/lists.py | 5 | ||||
| -rw-r--r-- | src/mailman/rest/plugins.py | 70 | ||||
| -rw-r--r-- | src/mailman/rest/root.py | 15 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_lists.py | 21 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_plugins.py | 105 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_systemconf.py | 2 |
8 files changed, 283 insertions, 6 deletions
diff --git a/src/mailman/rest/docs/lists.rst b/src/mailman/rest/docs/lists.rst index 6a034df94..67cbb697a 100644 --- a/src/mailman/rest/docs/lists.rst +++ b/src/mailman/rest/docs/lists.rst @@ -191,10 +191,13 @@ Apply a style at list creation time of a particular type, e.g. discussion lists. We can see which styles are available, and which is the default style. - >>> dump_json('http://localhost:9001/3.0/lists/styles') - default: legacy-default - http_etag: "..." - style_names: ['legacy-announce', 'legacy-default'] + >>> json = call_http('http://localhost:9001/3.0/lists/styles') + >>> json['default'] + 'legacy-default' + >>> for style in json['styles']: + ... print('{}: {}'.format(style['name'], style['description'])) + legacy-announce: Announce only mailing list style. + legacy-default: Ordinary discussion mailing list style. When creating a list, if we don't specify a style to apply, the default style is used. However, we can provide a style name in the POST data to choose a diff --git a/src/mailman/rest/docs/plugins.rst b/src/mailman/rest/docs/plugins.rst new file mode 100644 index 000000000..737a911c0 --- /dev/null +++ b/src/mailman/rest/docs/plugins.rst @@ -0,0 +1,60 @@ +========= + Plugins +========= + +Plugins can supply REST routes. +:: + + >>> dump_json('http://localhost:9001/3.1/plugins') + entry 0: + class: mailman.testing.plugin.ExamplePlugin + configuration: + enable: True + http_etag: "..." + name: example + path: + http_etag: "..." + start: 0 + total_size: 1 + + >>> dump_json('http://localhost:9001/3.1/plugins/example') + example-plugin-reply: yes + http_etag: "..." + + >>> dump_json('http://localhost:9001/3.1/plugins/example/good') + good: True + http_etag: "..." + + >>> dump_json('http://localhost:9001/3.1/plugins/example/bad') + Traceback (most recent call last): + ... + urllib.error.HTTPError: HTTP Error 400: ... + +For example this route that presents a counter. +:: + + >>> dump_json('http://localhost:9001/3.1/plugins/example/count') + count: 0 + http_etag: "..." + + >>> call_http('http://localhost:9001/3.1/plugins/example/count', + ... {'count':3}, method='POST') + content-length: 0 + date: ... + server: ... + status: 204 + + >>> dump_json('http://localhost:9001/3.1/plugins/example/count') + count: 3 + http_etag: "..." + + >>> call_http('http://localhost:9001/3.1/plugins/example/count', + ... method='DELETE') + content-length: 0 + date: ... + server: ... + status: 204 + + >>> dump_json('http://localhost:9001/3.1/plugins/example/count') + count: 0 + http_etag: "..." diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py index 4b467afb3..482d3e2d7 100644 --- a/src/mailman/rest/lists.py +++ b/src/mailman/rest/lists.py @@ -414,9 +414,10 @@ class Styles: def __init__(self): manager = getUtility(IStyleManager) - style_names = sorted(style.name for style in manager.styles) + styles = [dict(name=style.name, description=style.description) + for style in manager.styles] self._resource = dict( - style_names=style_names, + styles=styles, default=config.styles.default) def on_get(self, request, response): diff --git a/src/mailman/rest/plugins.py b/src/mailman/rest/plugins.py new file mode 100644 index 000000000..fa2605f24 --- /dev/null +++ b/src/mailman/rest/plugins.py @@ -0,0 +1,70 @@ +# Copyright (C) 2010-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 <http://www.gnu.org/licenses/>. + +"""REST for plugins, dynamically proxies requests to plugin's rest_object.""" + +from lazr.config import as_boolean +from mailman.config import config +from mailman.rest.helpers import CollectionMixin, NotFound, etag, okay +from public import public + + +@public +class AllPlugins(CollectionMixin): + """Read-only list of all plugin configs.""" + + def _resource_as_dict(self, plugin_config): + """See `CollectionMixin`.""" + name, plugin_section = plugin_config + resource = { + 'name': name, + 'class': plugin_section['class'], + 'enable': as_boolean(plugin_section['enable']), + 'path': plugin_section['path'], + 'configuration': plugin_section['configuration'] + } + return resource + + def _get_collection(self, request): + """See `CollectionMixin`.""" + return sorted(config.plugin_configs) + + def on_get(self, request, response): + """/plugins""" + resource = self._make_collection(request) + okay(response, etag(resource)) + + +@public +class APlugin: + """REST proxy to the plugins rest_object.""" + + def __init__(self, plugin_name): + self.__rest_object = None + if plugin_name in config.plugins.keys(): + plugin = config.plugins[plugin_name] + self.__rest_object = plugin.rest_object() + # If the plugin doesn't exist or doesn't provide a rest_object, + # just proxy to NotFound + if self.__rest_object is None: + self.__rest_object = NotFound() + + def __getattr__(self, attrib): + return getattr(self.__rest_object, attrib) + + def __dir__(self): + return dir(self.__rest_object) diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py index 76289078b..46116aba6 100644 --- a/src/mailman/rest/root.py +++ b/src/mailman/rest/root.py @@ -30,6 +30,7 @@ from mailman.rest.helpers import ( BadRequest, NotFound, child, etag, no_content, not_found, okay) from mailman.rest.lists import AList, AllLists, Styles from mailman.rest.members import AMember, AllMembers, FindMembers +from mailman.rest.plugins import APlugin, AllPlugins from mailman.rest.preferences import ReadOnlyPreferences from mailman.rest.queues import AQueue, AQueueFile, AllQueues from mailman.rest.templates import TemplateFinder @@ -308,6 +309,20 @@ class TopLevel: return BadRequest(), [] @child() + def plugins(self, context, segments): + """/<api>/plugins + /<api>/plugins/<plugin_name> + /<api>/plugins/<plugin_name>/... + """ + if self.api.version_info < (3, 1): + return NotFound(), [] + if len(segments) == 0: + return AllPlugins(), [] + else: + plugin_name = segments.pop(0) + return APlugin(plugin_name), segments + + @child() def bans(self, context, segments): """/<api>/bans /<api>/bans/<email> diff --git a/src/mailman/rest/tests/test_lists.py b/src/mailman/rest/tests/test_lists.py index 0f3b99393..44446b289 100644 --- a/src/mailman/rest/tests/test_lists.py +++ b/src/mailman/rest/tests/test_lists.py @@ -347,6 +347,27 @@ class TestLists(unittest.TestCase): self.assertEqual(cm.exception.reason, 'Missing parameters: emails') +class TestListStyles(unittest.TestCase): + """Test /lists/styles.""" + + layer = RESTLayer + + def test_styles(self): + json, response = call_api('http://localhost:9001/3.0/lists/styles') + self.assertEqual(response.status_code, 200) + # Remove the variable data. + json.pop('http_etag') + self.assertEqual(json, { + 'styles': [ + {'name': 'legacy-announce', + 'description': 'Announce only mailing list style.'}, + {'name': 'legacy-default', + 'description': 'Ordinary discussion mailing list style.'} + ], + 'default': 'legacy-default' + }) + + class TestListArchivers(unittest.TestCase): """Test corner cases for list archivers.""" diff --git a/src/mailman/rest/tests/test_plugins.py b/src/mailman/rest/tests/test_plugins.py new file mode 100644 index 000000000..9c12e1160 --- /dev/null +++ b/src/mailman/rest/tests/test_plugins.py @@ -0,0 +1,105 @@ +# Copyright (C) 2010-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 <http://www.gnu.org/licenses/>. + +"""Test RESTability of plugins.""" + +import unittest + +from mailman.testing.helpers import call_api +from mailman.testing.layers import RESTLayer +from urllib.error import HTTPError + + +class TestAPI31Plugins(unittest.TestCase): + layer = RESTLayer + + def test_list_plugins(self): + json, response = call_api('http://localhost:9001/3.1/plugins') + self.assertEqual(response.status_code, 200) + json.pop('http_etag') + self.assertEqual(len(json.keys()), 3) + self.assertIn('entries', json.keys()) + entries = json['entries'] + self.assertEqual(len(entries), 1) + plugin_conf = entries[0] + self.assertEqual(plugin_conf['name'], 'example') + self.assertEqual(plugin_conf['enable'], True) + self.assertEqual(plugin_conf['class'], + 'mailman.testing.plugin.ExamplePlugin') + self.assertEqual(plugin_conf['path'], '') + self.assertEqual(plugin_conf['configuration'], '') + + def test_non_existent_plugin(self): + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.1/plugins/does-not-exist') + self.assertEqual(cm.exception.code, 404) + + def test_example_plugin(self): + json, response = call_api('http://localhost:9001/3.1/plugins/example') + self.assertEqual(response.status_code, 200) + json.pop('http_etag') + self.assertEqual(len(json.keys()), 1) + self.assertEqual(json['example-plugin-reply'], 'yes') + + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.1/plugins/example', + method='POST') + self.assertEqual(cm.exception.code, 400) + + def test_example_plugin_child(self): + json, response = \ + call_api('http://localhost:9001/3.1/plugins/example/good') + self.assertEqual(response.status_code, 200) + json.pop('http_etag') + self.assertEqual(len(json.keys()), 1) + self.assertEqual(json['good'], True) + + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.1/plugins/example/bad') + self.assertEqual(cm.exception.code, 400) + + def test_example_plugin_counter(self): + json, response = \ + call_api('http://localhost:9001/3.1/plugins/example/count') + self.assertEqual(response.status_code, 200) + json.pop('http_etag') + self.assertEqual(len(json.keys()), 1) + self.assertEqual(json['count'], 0) + + json, response = \ + call_api('http://localhost:9001/3.1/plugins/example/count', + method='POST', data={'count': 5}) + self.assertEqual(response.status_code, 204) + + json, response = \ + call_api('http://localhost:9001/3.1/plugins/example/count') + self.assertEqual(response.status_code, 200) + json.pop('http_etag') + self.assertEqual(len(json.keys()), 1) + self.assertEqual(json['count'], 5) + + json, response = \ + call_api('http://localhost:9001/3.1/plugins/example/count', + method='DELETE') + self.assertEqual(response.status_code, 204) + + json, response = \ + call_api('http://localhost:9001/3.1/plugins/example/count') + self.assertEqual(response.status_code, 200) + json.pop('http_etag') + self.assertEqual(len(json.keys()), 1) + self.assertEqual(json['count'], 0) diff --git a/src/mailman/rest/tests/test_systemconf.py b/src/mailman/rest/tests/test_systemconf.py index f9fe9caa0..59f0619a3 100644 --- a/src/mailman/rest/tests/test_systemconf.py +++ b/src/mailman/rest/tests/test_systemconf.py @@ -168,6 +168,7 @@ class TestSystemConfiguration(unittest.TestCase): 'logging.http', 'logging.locks', 'logging.mischief', + 'logging.plugin', 'logging.root', 'logging.runner', 'logging.smtp', @@ -182,6 +183,7 @@ class TestSystemConfiguration(unittest.TestCase): 'paths.here', 'paths.local', 'paths.testing', + 'plugin.example', 'runner.archive', 'runner.bad', 'runner.bounces', |
