diff options
| author | Barry Warsaw | 2015-02-13 03:13:06 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2015-02-13 03:13:06 -0500 |
| commit | 6d2c66ce133cd2c119fcb462dff662621013631a (patch) | |
| tree | 8ce6043cc9e95fc9bcb5ba5dab720e8b73a8c56b /src | |
| parent | 7ffb6d2d43471486997c78c3cffa787a10560ecf (diff) | |
| download | mailman-6d2c66ce133cd2c119fcb462dff662621013631a.tar.gz mailman-6d2c66ce133cd2c119fcb462dff662621013631a.tar.zst mailman-6d2c66ce133cd2c119fcb462dff662621013631a.zip | |
* A new API is provided to support non-production testing infrastructures,
allowing a client to cull all orphaned UIDs via ``DELETE`` on
``<api>/reserved/uids/orphans``. Note that *no guarantees* of API
stability will ever be made for resources under ``reserved``.
(LP: #1420083)
Also:
- Allow @dbconnection methods to be @staticmethods taking only one argument,
the store to perform the query on.
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/database/transaction.py | 7 | ||||
| -rw-r--r-- | src/mailman/docs/NEWS.rst | 5 | ||||
| -rw-r--r-- | src/mailman/model/tests/test_uid.py | 41 | ||||
| -rw-r--r-- | src/mailman/model/uid.py | 17 | ||||
| -rw-r--r-- | src/mailman/rest/root.py | 30 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_root.py | 8 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_uids.py | 76 |
7 files changed, 181 insertions, 3 deletions
diff --git a/src/mailman/database/transaction.py b/src/mailman/database/transaction.py index 30710017e..b96562d3f 100644 --- a/src/mailman/database/transaction.py +++ b/src/mailman/database/transaction.py @@ -70,6 +70,9 @@ def dbconnection(function): attribute. This calls the function with `store` as the first argument. """ def wrapper(*args, **kws): - # args[0] is self. - return function(args[0], config.db.store, *args[1:], **kws) + # args[0] is self, if there is one. + if len(args) > 0: + return function(args[0], config.db.store, *args[1:], **kws) + else: + return function(config.db.store, **kws) return wrapper diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst index 4cc5fc2e3..116747382 100644 --- a/src/mailman/docs/NEWS.rst +++ b/src/mailman/docs/NEWS.rst @@ -44,6 +44,11 @@ REST from the various queue directories via the ``<api>/queues`` resource. * You can now DELETE an address. If the address is linked to a user, the user is not delete, it is just unlinked. + * A new API is provided to support non-production testing infrastructures, + allowing a client to cull all orphaned UIDs via ``DELETE`` on + ``<api>/reserved/uids/orphans``. Note that *no guarantees* of API + stability will ever be made for resources under ``reserved``. + (LP: #1420083) 3.0 beta 5 -- "Carve Away The Stone" diff --git a/src/mailman/model/tests/test_uid.py b/src/mailman/model/tests/test_uid.py index d36fa4c3b..8f3b4af70 100644 --- a/src/mailman/model/tests/test_uid.py +++ b/src/mailman/model/tests/test_uid.py @@ -25,8 +25,11 @@ __all__ = [ import uuid import unittest +from mailman.config import config +from mailman.interfaces.usermanager import IUserManager from mailman.model.uid import UID from mailman.testing.layers import ConfigLayer +from zope.component import getUtility @@ -44,3 +47,41 @@ class TestUID(unittest.TestCase): my_uuid = uuid.uuid4() UID.record(my_uuid) self.assertRaises(ValueError, UID.record, my_uuid) + + def test_get_total_uid_count(self): + # The reserved REST API needs this. + for i in range(10): + UID.record(uuid.uuid4()) + self.assertEqual(UID.get_total_uid_count(), 10) + + def test_cull_orphan_uids(self): + # The reserved REST API needs to cull entries from the uid table that + # are not associated with actual entries in the user table. + manager = getUtility(IUserManager) + uids = set() + for i in range(10): + user = manager.create_user() + uids.add(user.user_id) + # The testing infrastructure does not record the UIDs for new user + # objects, so do that now to mimic the real system. + UID.record(user.user_id) + self.assertEqual(len(uids), 10) + # Now add some orphan uids. + orphans = set() + for i in range(100, 113): + uid = UID.record(uuid.UUID(int=i)) + orphans.add(uid.uid) + self.assertEqual(len(orphans), 13) + # Normally we wouldn't do a query in a test, since we'd want the model + # object to expose this, but we actually don't support exposing all + # the UIDs to the rest of Mailman. + all_uids = set(row[0] for row in config.db.store.query(UID.uid)) + self.assertEqual(all_uids, uids | orphans) + # Now, cull all the UIDs that aren't associated with users. Do use + # the model API for this. + UID.cull_orphans() + non_orphans = set(row[0] for row in config.db.store.query(UID.uid)) + self.assertEqual(uids, non_orphans) + # And all the users still exist. + non_orphans = set(user.user_id for user in manager.users) + self.assertEqual(uids, non_orphans) diff --git a/src/mailman/model/uid.py b/src/mailman/model/uid.py index c0d3e4d4d..0ff22438c 100644 --- a/src/mailman/model/uid.py +++ b/src/mailman/model/uid.py @@ -74,3 +74,20 @@ class UID(Model): if existing.count() != 0: raise ValueError(uid) return UID(uid) + + @staticmethod + @dbconnection + def get_total_uid_count(store): + return store.query(UID).count() + + @staticmethod + @dbconnection + def cull_orphans(store): + # Avoid circular imports. + from mailman.model.user import User + # Delete all uids in this table that are not associated with user + # rows. + results = store.query(UID).filter( + ~UID.uid.in_(store.query(User._user_id))) + for uid in results.all(): + store.delete(uid) diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py index d4dca146e..0861a9a5b 100644 --- a/src/mailman/rest/root.py +++ b/src/mailman/rest/root.py @@ -29,10 +29,11 @@ from mailman.config import config from mailman.core.constants import system_preferences from mailman.core.system import system from mailman.interfaces.listmanager import IListManager +from mailman.model.uid import UID from mailman.rest.addresses import AllAddresses, AnAddress from mailman.rest.domains import ADomain, AllDomains from mailman.rest.helpers import ( - BadRequest, NotFound, child, etag, not_found, okay, path_to) + BadRequest, NotFound, child, etag, no_content, not_found, okay, path_to) from mailman.rest.lists import AList, AllLists, Styles from mailman.rest.members import AMember, AllMembers, FindMembers from mailman.rest.preferences import ReadOnlyPreferences @@ -42,6 +43,9 @@ from mailman.rest.users import AUser, AllUsers from zope.component import getUtility +SLASH = '/' + + class Root: """The RESTful root resource. @@ -110,6 +114,25 @@ class SystemConfiguration: okay(response, etag(resource)) +class Reserved: + """Top level API for reserved operations. + + Nothing under this resource should be considered part of the stable API. + The resources that appear here are purely for the support of external + non-production systems, such as testing infrastructures for cooperating + components. Use at your own risk. + """ + def __init__(self, segments): + self._resource_path = SLASH.join(segments) + + def on_delete(self, request, response): + if self._resource_path != 'uids/orphans': + not_found(response) + return + UID.cull_orphans() + no_content(response) + + class TopLevel: """Top level collections and entries.""" @@ -226,3 +249,8 @@ class TopLevel: return AQueueFile(segments[0], segments[1]), [] else: return BadRequest(), [] + + @child() + def reserved(self, request, segments): + """/<api>/reserved/[...]""" + return Reserved(segments), [] diff --git a/src/mailman/rest/tests/test_root.py b/src/mailman/rest/tests/test_root.py index 6d10fc635..905461e46 100644 --- a/src/mailman/rest/tests/test_root.py +++ b/src/mailman/rest/tests/test_root.py @@ -123,3 +123,11 @@ class TestRoot(unittest.TestCase): self.assertEqual(content['title'], '401 Unauthorized') self.assertEqual(content['description'], 'User is not authorized for the REST API') + + def test_reserved_bad_subpath(self): + # Only <api>/reserved/uids/orphans is a defined resource. DELETEing + # anything else gives a 404. + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/reserved/uids/assigned', + method='DELETE') + self.assertEqual(cm.exception.code, 404) diff --git a/src/mailman/rest/tests/test_uids.py b/src/mailman/rest/tests/test_uids.py new file mode 100644 index 000000000..6c31a8aa4 --- /dev/null +++ b/src/mailman/rest/tests/test_uids.py @@ -0,0 +1,76 @@ +# Copyright (C) 2015 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 deletion of orphaned UIDs. + +There is no doctest for this functionality, since it's only useful for testing +of external clients of the REST API. +""" + +__all__ = [ + 'TestUIDs', + ] + + +import unittest + +from mailman.config import config +from mailman.database.transaction import transaction +from mailman.interfaces.usermanager import IUserManager +from mailman.model.uid import UID +from mailman.testing.helpers import call_api +from mailman.testing.layers import RESTLayer +from zope.component import getUtility + + + +class TestUIDs(unittest.TestCase): + layer = RESTLayer + + def test_delete_orphans(self): + # When users are deleted, their UIDs are generally not deleted. We + # never delete rows from that table in order to guarantee no + # duplicates. However, some external testing frameworks want to be + # able to reset the UID table, so they can use this interface to do + # so. See LP: #1420083. + # + # Create some users. + manager = getUtility(IUserManager) + users_by_uid = {} + with transaction(): + for i in range(10): + user = manager.create_user() + users_by_uid[user.user_id] = user + # The testing infrastructure does not record the UIDs for new + # user options, so do that now to mimic the real system. + UID.record(user.user_id) + # We now have 10 unique uids. + self.assertEqual(len(users_by_uid), 10) + # Now delete all the users. + with transaction(): + for user in list(users_by_uid.values()): + manager.delete_user(user) + # There are still 10 unique uids in the database. + self.assertEqual(UID.get_total_uid_count(), 10) + # Cull the orphan UIDs. + content, response = call_api( + 'http://localhost:9001/3.0/reserved/uids/orphans', + method='DELETE') + self.assertEqual(response.status, 204) + # Now there are no uids in the table. + config.db.abort() + self.assertEqual(UID.get_total_uid_count(), 0) |
