From 33ad44bc97f08df71f227f6f2a006e770a75c353 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Fri, 1 Apr 2011 18:51:29 -0400 Subject: * Re-organize the interface between buildout.cfg and the zope.testing layer initialization. buildout.cfg is now really simple; it calls one method. That method does all the relevant layer initializations. This better localizes what has to be set up before testing can even begin. * IUsers now have a created_on property which contains the datetime at which the user record was created. * Rework the date and uid factories so that they consult the MockAndMonkeyLayer for the current testing flag. Also, those factories register themselves with the layer so that they'll get automatically reset between tests, without the layer actually having to know about them. * Move the User model object initialization into User.__init__() from the user manager. The User now also adds itself to the store. * Add a 'uid factory' for unique id creation, which is test suite aware. --- src/mailman/database/mailman.sql | 1 + src/mailman/interfaces/user.py | 3 ++ src/mailman/model/docs/users.txt | 15 ++++++-- src/mailman/model/user.py | 18 ++++++++- src/mailman/model/usermanager.py | 15 +------- src/mailman/testing/__init__.py | 39 +++++++++++++++++++ src/mailman/testing/layers.py | 18 +++++---- src/mailman/tests/test_documentation.py | 4 +- src/mailman/utilities/datetime.py | 11 ++++-- src/mailman/utilities/uid.py | 67 +++++++++++++++++++++++++++++++++ 10 files changed, 159 insertions(+), 32 deletions(-) create mode 100644 src/mailman/utilities/uid.py (limited to 'src') diff --git a/src/mailman/database/mailman.sql b/src/mailman/database/mailman.sql index 8d5a424a3..7d67dea05 100644 --- a/src/mailman/database/mailman.sql +++ b/src/mailman/database/mailman.sql @@ -253,6 +253,7 @@ CREATE TABLE user ( real_name TEXT, password TEXT, _user_id TEXT, + _created_on TIMESTAMP, preferences_id INTEGER, PRIMARY KEY (id), CONSTRAINT user_preferences_id_fk diff --git a/src/mailman/interfaces/user.py b/src/mailman/interfaces/user.py index 18655e4d8..2c2652413 100644 --- a/src/mailman/interfaces/user.py +++ b/src/mailman/interfaces/user.py @@ -41,6 +41,9 @@ class IUser(Interface): user_id = Attribute( """The user's unique, random, identifier (sha1 hex digest).""") + created_on = Attribute( + """The date and time at which this user was created.""") + addresses = Attribute( """An iterator over all the `IAddresses` controlled by this user.""") diff --git a/src/mailman/model/docs/users.txt b/src/mailman/model/docs/users.txt index 1703db1ee..29d8601cd 100644 --- a/src/mailman/model/docs/users.txt +++ b/src/mailman/model/docs/users.txt @@ -36,15 +36,16 @@ The password and real name can be changed at any time. another password -User id -======= +Basic user identification +========================= Although rarely visible to users, every user has a unique ID in Mailman, which never changes. This ID is generated randomly at the time the user is created. - >>> print len(user_1.user_id) - 40 + # The test suite uses a predictable user id. + >>> print user_1.user_id + 1 The user id cannot change. @@ -53,6 +54,12 @@ The user id cannot change. ... AttributeError: can't set attribute +User records also have a date on which they where created. + + # The test suite uses a predictable timestamp. + >>> print user_1.created_on + 2005-08-01 07:49:23 + Users addresses =============== diff --git a/src/mailman/model/user.py b/src/mailman/model/user.py index f037bdd48..05ce356ca 100644 --- a/src/mailman/model/user.py +++ b/src/mailman/model/user.py @@ -24,7 +24,7 @@ __all__ = [ 'User', ] -from storm.locals import Int, Reference, ReferenceSet, Unicode +from storm.locals import DateTime, Int, Reference, ReferenceSet, Unicode from zope.interface import implements from mailman.config import config @@ -35,6 +35,8 @@ from mailman.interfaces.user import IUser from mailman.model.address import Address from mailman.model.preferences import Preferences from mailman.model.roster import Memberships +from mailman.utilities.datetime import factory as date_factory +from mailman.utilities.uid import factory as uid_factory @@ -47,11 +49,20 @@ class User(Model): real_name = Unicode() password = Unicode() _user_id = Unicode() + _created_on = DateTime() addresses = ReferenceSet(id, 'Address.user_id') preferences_id = Int() preferences = Reference(preferences_id, 'Preferences.id') + def __init__(self, real_name=None, preferences=None): + super(User, self).__init__() + self._created_on = date_factory.now() + self._user_id = uid_factory.new_uid() + self.real_name = ('' if real_name is None else real_name) + self.preferences = preferences + config.db.store.add(self) + def __repr__(self): return ''.format( self, id(self)) @@ -61,6 +72,11 @@ class User(Model): """See `IUser`.""" return self._user_id + @property + def created_on(self): + """See `IUser`.""" + return self._created_on + def link(self, address): """See `IUser`.""" if address.user is not None: diff --git a/src/mailman/model/usermanager.py b/src/mailman/model/usermanager.py index 3294b3e7f..d6817021d 100644 --- a/src/mailman/model/usermanager.py +++ b/src/mailman/model/usermanager.py @@ -25,10 +25,6 @@ __all__ = [ ] -import os -import time -import hashlib - from zope.interface import implements from mailman.config import config @@ -37,7 +33,6 @@ from mailman.interfaces.usermanager import IUserManager from mailman.model.address import Address from mailman.model.preferences import Preferences from mailman.model.user import User -from mailman.utilities.passwords import SALT_LENGTH @@ -45,18 +40,10 @@ class UserManager: implements(IUserManager) def create_user(self, email=None, real_name=None): - user = User() - user.real_name = ('' if real_name is None else real_name) + user = User(real_name, Preferences()) if email: address = self.create_address(email, real_name) user.link(address) - user.preferences = Preferences() - # Generate a unique random SHA1 hash for the user id. - salt = os.urandom(SALT_LENGTH) - h = hashlib.sha1(repr(time.time())) - h.update(salt) - user._user_id = unicode(h.hexdigest(), 'us-ascii') - config.db.store.add(user) return user def delete_user(self, user): diff --git a/src/mailman/testing/__init__.py b/src/mailman/testing/__init__.py index e69de29bb..84182e1f1 100644 --- a/src/mailman/testing/__init__.py +++ b/src/mailman/testing/__init__.py @@ -0,0 +1,39 @@ +# Copyright (C) 2011 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 . + +"""Set up testing. + +This is used as an interface to buildout.cfg's [test] section. +zope.testrunner supports an initialization variable. It is set to import and +run the following test initialization method. +""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'initialize', + ] + + + +def initialize(root_directory): + """Initialize the test infrastructure.""" + from mailman.testing import layers + layers.MockAndMonkeyLayer.testing_mode = True + layers.ConfigLayer.enable_stderr(); + layers.ConfigLayer.set_root_directory(root_directory) diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py index 353dd9edd..2e765ea3e 100644 --- a/src/mailman/testing/layers.py +++ b/src/mailman/testing/layers.py @@ -48,7 +48,6 @@ from mailman.core.logging import get_handler from mailman.interfaces.domain import IDomainManager from mailman.testing.helpers import TestableMaster, reset_the_world from mailman.testing.mta import ConnectionCountingController -from mailman.utilities.datetime import factory from mailman.utilities.string import expand @@ -60,17 +59,20 @@ NL = '\n' class MockAndMonkeyLayer: """Layer for mocking and monkey patching for testing.""" - @classmethod - def setUp(cls): - factory.testing_mode = True + # Set this to True to enable predictable datetimes, uids, etc. + testing_mode = False - @classmethod - def tearDown(cls): - factory.testing_mode = False + # A registration of all testing factories, for resetting between tests. + _resets = [] @classmethod def testTearDown(cls): - factory.reset() + for reset in cls._resets: + reset() + + @classmethod + def register_reset(cls, reset): + cls._resets.append(reset) diff --git a/src/mailman/tests/test_documentation.py b/src/mailman/tests/test_documentation.py index 23cc189d0..2a72a367f 100644 --- a/src/mailman/tests/test_documentation.py +++ b/src/mailman/tests/test_documentation.py @@ -126,7 +126,7 @@ def dump_list(list_of_things, key=str): def call_http(url, data=None, method=None, username=None, password=None): - """'Call' a URL with a given HTTP method and return the resulting object. + """'Call a URL with a given HTTP method and return the resulting object. The object will have been JSON decoded. @@ -142,6 +142,8 @@ def call_http(url, data=None, method=None, username=None, password=None): :param password: The HTTP Basic Auth password. None means use the value from the configuration. :type username: str + :return: The decoded JSON data structure. + :raises HTTPError: when a non-2xx return code is received. """ headers = {} if data is not None: diff --git a/src/mailman/utilities/datetime.py b/src/mailman/utilities/datetime.py index 7e727346d..9dcd21f1e 100644 --- a/src/mailman/utilities/datetime.py +++ b/src/mailman/utilities/datetime.py @@ -36,25 +36,27 @@ __all__ = [ import datetime +from mailman.testing.layers import MockAndMonkeyLayer + class DateFactory: """A factory for today() and now() that works with testing.""" - # Set to True to produce predictable dates and times. - testing_mode = False # The predictable time. predictable_now = None predictable_today = None def now(self, tz=None): + # We can't automatically fast-forward because some tests require us to + # stay on the same day for a while, e.g. autorespond.txt. return (self.predictable_now - if self.testing_mode + if MockAndMonkeyLayer.testing_mode else datetime.datetime.now(tz)) def today(self): return (self.predictable_today - if self.testing_mode + if MockAndMonkeyLayer.testing_mode else datetime.date.today()) @classmethod @@ -72,3 +74,4 @@ factory = DateFactory() factory.reset() today = factory.today now = factory.now +MockAndMonkeyLayer.register_reset(factory.reset) diff --git a/src/mailman/utilities/uid.py b/src/mailman/utilities/uid.py new file mode 100644 index 000000000..cc2cf3b12 --- /dev/null +++ b/src/mailman/utilities/uid.py @@ -0,0 +1,67 @@ +# Copyright (C) 2011 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 . + +"""Unique ID generation. + +Use these functions to create unique ids rather than inlining calls to hashlib +and whatnot. These are better instrumented for testing purposes. +""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'UniqueIDFactory', + 'factory', + ] + + +import os +import time +import hashlib + +from mailman.testing.layers import MockAndMonkeyLayer +from mailman.utilities.passwords import SALT_LENGTH + + + +class UniqueIDFactory: + """A factory for unique ids.""" + + # The predictable id. + predictable_id = None + + def new_uid(self, bytes=None): + if MockAndMonkeyLayer.testing_mode: + uid = self.predictable_id + self.predictable_id += 1 + return unicode(uid) + salt = os.urandom(SALT_LENGTH) + h = hashlib.sha1(repr(time.time())) + h.update(salt) + if bytes is not None: + h.update(bytes) + return unicode(h.hexdigest(), 'us-ascii') + + def reset(self): + self.predictable_id = 1 + + + +factory = UniqueIDFactory() +factory.reset() +MockAndMonkeyLayer.register_reset(factory.reset) -- cgit v1.2.3-70-g09d2