summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2015-02-13 03:13:06 -0500
committerBarry Warsaw2015-02-13 03:13:06 -0500
commit6d2c66ce133cd2c119fcb462dff662621013631a (patch)
tree8ce6043cc9e95fc9bcb5ba5dab720e8b73a8c56b /src
parent7ffb6d2d43471486997c78c3cffa787a10560ecf (diff)
downloadmailman-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.py7
-rw-r--r--src/mailman/docs/NEWS.rst5
-rw-r--r--src/mailman/model/tests/test_uid.py41
-rw-r--r--src/mailman/model/uid.py17
-rw-r--r--src/mailman/rest/root.py30
-rw-r--r--src/mailman/rest/tests/test_root.py8
-rw-r--r--src/mailman/rest/tests/test_uids.py76
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)