[Checkins] SVN: Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/ Add possibility to transform the login name.

Maurits van Rees cvs-admin at zope.org
Fri Dec 28 15:33:06 UTC 2012


Log message for revision 128927:
  Add possibility to transform the login name.
  
  The BasePlugin now has a property 'login_transform' and a method 'applyTransform'.
  In proper places, plugins can call 'login_name = self.applyTransform(login_name)'.
  When 'login_transform' is 'lower', this method will return 'self.lower(login_name)'.
  Care is taken to not fail when the method does not exist.  The original login_name
  is then returned.
  A use case is to transform all login names to lowercase if you want to use the email
  address as login name in Plone.
  The ZODBUserManager and CookieAuthHelper plugins use this now,
  though it is not strictly needed for the last one.
  More may follow.
  

Changed:
  U   Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/BasePlugin.py
  U   Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/CookieAuthHelper.py
  U   Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/ZODBUserManager.py
  U   Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/tests/test_CookieAuthHelper.py
  U   Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/tests/test_ZODBUserManager.py

-=-
Modified: Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/BasePlugin.py
===================================================================
--- Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/BasePlugin.py	2012-12-28 15:08:19 UTC (rev 128926)
+++ Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/BasePlugin.py	2012-12-28 15:33:06 UTC (rev 128927)
@@ -15,6 +15,8 @@
 
 $Id$
 """
+import logging
+
 from OFS.SimpleItem import SimpleItem
 from OFS.PropertyManager import PropertyManager
 from Acquisition import aq_base, aq_parent, aq_inner
@@ -30,6 +32,9 @@
 from Products.PluggableAuthService.permissions import ManageUsers
 from Products.PluggableAuthService.utils import createViewName
 
+LOG = logging.getLogger('PluggableAuthService')
+
+
 def flattenInterfaces(implemented):
     return implemented.flattened()
 
@@ -50,10 +55,15 @@
                      )
 
     prefix = ''
+    # Method for transforming a login name.  This needs to be the name
+    # of a method on this plugin.  See the applyTransform method.
+    login_transform = ''
 
     _properties = (
         dict(id='prefix', type='string', mode='w',
-             label='Optional Prefix'),)
+             label='Optional Prefix'),
+        dict(id='login_transform', type='string', mode='w',
+             label='Transform to apply to login name'))
 
     security.declareProtected( ManageUsers, 'manage_activateInterfacesForm' )
     manage_activateInterfacesForm = PageTemplateFile(
@@ -105,6 +115,47 @@
                               'Interface+activations+updated.'
                             % self.absolute_url())
 
+    security.declarePublic( 'applyTransform' )
+    def applyTransform( self, value ):
+        """ Transform for login name.
+
+        Possibly transform the login, for example by making it lower
+        case.
+
+        value must be a string (or unicode).
+        """
+        login_transform = getattr(self, 'login_transform', None)
+        if not login_transform:
+            return value
+        if not value:
+            return value
+        transform = getattr(self, login_transform.strip(), None)
+        if transform is None:
+            LOG.debug("Transform method %r not found in plugin %r.",
+                      self.login_transform, self)
+            return value
+        return transform(value)
+
+    security.declarePublic( 'lower' )
+    def lower( self, value ):
+        """ Transform for login name.
+
+        Strip the value and lowercase it.
+
+        To use this, set login_tranform to 'lower'.
+        """
+        return value.strip().lower()
+
+    security.declarePublic( 'upper' )
+    def upper( self, value ):
+        """ Transform for login name.
+
+        Strip the value and uppercase it.
+
+        To use this, set login_tranform to 'upper'.
+        """
+        return value.strip().upper()
+
     security.declarePrivate( '_getPAS' )
     def _getPAS( self ):
         """ Canonical way to get at the PAS instance from a plugin """

Modified: Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/CookieAuthHelper.py
===================================================================
--- Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/CookieAuthHelper.py	2012-12-28 15:08:19 UTC (rev 128926)
+++ Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/CookieAuthHelper.py	2012-12-28 15:33:06 UTC (rev 128927)
@@ -114,7 +114,7 @@
         login = request.form.get('__ac_name', '')
 
         if login and request.form.has_key('__ac_password'):
-            creds['login'] = login
+            creds['login'] = self.applyTransform(login)
             creds['password'] = request.form.get('__ac_password', '')
 
         elif cookie and cookie != 'deleted':
@@ -137,6 +137,7 @@
             except TypeError:
                 # Cookie is in a different format, so it is not ours
                 return {}
+            creds['login'] = self.applyTransform(creds['login'])
 
         if creds:
             creds['remote_host'] = request.get('REMOTE_HOST', '')
@@ -158,6 +159,7 @@
     security.declarePrivate('updateCredentials')
     def updateCredentials(self, request, response, login, new_password):
         """ Respond to change of credentials (NOOP for basic auth). """
+        login = self.applyTransform(login)
         cookie_str = '%s:%s' % (login.encode('hex'), new_password.encode('hex'))
         cookie_val = encodestring(cookie_str)
         cookie_val = cookie_val.rstrip()

Modified: Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/ZODBUserManager.py
===================================================================
--- Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/ZODBUserManager.py	2012-12-28 15:08:19 UTC (rev 128926)
+++ Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/ZODBUserManager.py	2012-12-28 15:33:06 UTC (rev 128927)
@@ -103,20 +103,16 @@
         if login is None or password is None:
             return None
 
+        # Possibly transform the login, for example by making it lower
+        # case.  Standard behaviour is to do nothing.
+        login = self.applyTransform(login)
+
         # Do we have a link between login and userid?  Do NOT fall
         # back to using the login as userid when there is no match, as
         # that gives a high chance of seeming to log in successfully,
         # but in reality failing.
         userid = self._login_to_userid.get(login)
         if userid is None:
-            # Someone may be logging in with a userid instead of a
-            # login name and the two are not the same.  We could try
-            # turning those around, but really we should just fail.
-            #
-            # userid = login
-            # login = self._userid_to_login.get(userid)
-            # if login is None:
-            #     return None
             return None
 
         reference = self._user_passwords.get(userid)
@@ -163,6 +159,15 @@
         if isinstance( login, basestring ):
             login = [ login ]
 
+        if login:
+            # Possibly transform the login, for example by making it
+            # lower case.  Standard behaviour is to do nothing.
+            # Avoid changing the passed 'login' argument.
+            newlogin = []
+            for entry in login:
+                newlogin.append(self.applyTransform(entry))
+            login = newlogin
+
         # Look in the cache first...
         keywords = copy.deepcopy(kw)
         keywords.update( { 'id' : id
@@ -235,6 +240,7 @@
     #
     security.declarePrivate( 'doAddUser' )
     def doAddUser( self, login, password ):
+        login = self.applyTransform(login)
         try:
             self.addUser( login, login, password )
         except KeyError:
@@ -280,6 +286,7 @@
 
         o Raise KeyError if no user exists for the login name.
         """
+        login_name = self.applyTransform(login_name)
         return self._login_to_userid[ login_name ]
 
     security.declareProtected( ManageUsers, 'getLoginForUserId' )
@@ -294,6 +301,8 @@
     security.declarePrivate( 'addUser' )
     def addUser( self, user_id, login_name, password ):
 
+        login_name = self.applyTransform(login_name)
+
         if self._user_passwords.get( user_id ) is not None:
             raise KeyError, 'Duplicate user ID: %s' % user_id
 
@@ -311,6 +320,8 @@
     security.declarePrivate('updateUser')
     def updateUser(self, user_id, login_name):
 
+        login_name = self.applyTransform(login_name)
+
         # The following raises a KeyError if the user_id is invalid
         old_login = self.getLoginForUserId(user_id)
 
@@ -405,7 +416,8 @@
             if not login_name:
                 login_name = user_id
 
-            # XXX:  validate 'user_id', 'login_name' against policies?
+            # XXX:  validate 'user_id' against policies?
+            login_name = self.applyTransform(login_name)
 
             self.addUser( user_id, login_name, password )
 
@@ -456,7 +468,8 @@
         if not login_name:
             login_name = user_id
 
-        # XXX:  validate 'user_id', 'login_name' against policies?
+        # XXX:  validate 'user_id' against policies?
+        login_name = self.applyTransform(login_name)
 
         self.updateUser(user_id, login_name)
 
@@ -533,7 +546,8 @@
             if not login_name:
                 login_name = user_id
 
-            # XXX:  validate 'user_id', 'login_name' against policies?
+            # XXX:  validate 'user_id' against policies?
+            login_name = self.applyTransform(login_name)
             self.updateUser( user_id, login_name )
             self.updateUserPassword( user_id, password )
 
@@ -562,6 +576,9 @@
                 , **kw
                 ):
 
+        # Note: if login_transform is used by the plugin, then the
+        # login argument must have been transformed by applyTransform
+        # already.  That is the responsibility of the calling plugin.
         self._filter_ids = id
         self._filter_logins = login
         self._filter_keywords = kw

Modified: Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/tests/test_CookieAuthHelper.py
===================================================================
--- Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/tests/test_CookieAuthHelper.py	2012-12-28 15:08:19 UTC (rev 128926)
+++ Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/tests/test_CookieAuthHelper.py	2012-12-28 15:33:06 UTC (rev 128927)
@@ -100,6 +100,23 @@
                          'remote_address': ''})
         self.assertEqual(len(response.cookies), 0)
 
+    def test_extractCredentials_with_form_creds_lowercase( self ):
+
+        helper = self._makeOne()
+        helper.login_transform = 'lower'
+        response = FauxCookieResponse()
+        request = FauxSettableRequest(__ac_name='Foo',
+                                      __ac_password='B:Ar',
+                                      RESPONSE=response)
+
+        self.assertEqual(len(response.cookies), 0)
+        self.assertEqual(helper.extractCredentials(request),
+                        {'login': 'foo',
+                         'password': 'B:Ar',
+                         'remote_host': '',
+                         'remote_address': ''})
+        self.assertEqual(len(response.cookies), 0)
+
     def test_extractCredentials_with_deleted_cookie(self):
         # http://www.zope.org/Collectors/PAS/43
         # Edge case: The ZPublisher sets a cookie's value to "deleted"
@@ -173,6 +190,54 @@
         helper.login()
         self.assertEqual(len(response.cookies), 0)
 
+    def test_credentialsUpdate( self ):
+        from base64 import encodestring
+
+        helper = self._makeOne()
+        # Test lowercase transform.
+        helper.login_transform = 'lower'
+        response = FauxCookieResponse()
+        # Use mixed case login name here:
+        request = FauxSettableRequest( __ac_name='Foo'
+                                     , __ac_password='bar'
+                                     , RESPONSE=response
+                                     )
+        request.form['came_from'] = ''
+        helper.REQUEST = request
+
+        # Use mixed case login name here:
+        helper.updateCredentials(request, response, 'Foo', 'bar')
+        self.assertEqual(len(response.cookies), 1)
+
+        # Use lowercase login name here:
+        cookie_str = '%s:%s' % ('foo'.encode('hex'), 'bar'.encode('hex'))
+        cookie_val = encodestring(cookie_str)
+        cookie_val = cookie_val.rstrip()
+        self.assertEqual(response.cookies.get((helper.cookie_name, '/')),
+                         urllib.quote(cookie_val))
+
+        # Use the cookie for the next credentials extraction to see if
+        # it really works.
+        request = FauxSettableRequest(RESPONSE=response)
+        request.set(helper.cookie_name, cookie_val)
+        self.assertEqual(helper.extractCredentials(request),
+                        {'login': 'foo',
+                         'password': 'bar',
+                         'remote_host': '',
+                         'remote_address': ''})
+
+        # Use mixed case login name here:
+        cookie_str = '%s:%s' % ('Foo'.encode('hex'), 'bar'.encode('hex'))
+        cookie_val = encodestring(cookie_str)
+        cookie_val = cookie_val.rstrip()
+        request = FauxSettableRequest(RESPONSE=response)
+        request.set(helper.cookie_name, cookie_val)
+        self.assertEqual(helper.extractCredentials(request),
+                        {'login': 'foo',
+                         'password': 'bar',
+                         'remote_host': '',
+                         'remote_address': ''})
+
     def test_extractCredentials_from_cookie_with_colon_in_password(self):
         # http://www.zope.org/Collectors/PAS/51
         # Passwords with ":" characters broke authentication
@@ -211,7 +276,6 @@
 
     def test_extractCredentials_from_cookie_with_bad_binascii(self):
         # this might happen between browser implementations
-        from base64 import encodestring
 
         helper = self._makeOne()
         response = FauxCookieResponse()

Modified: Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/tests/test_ZODBUserManager.py
===================================================================
--- Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/tests/test_ZODBUserManager.py	2012-12-28 15:08:19 UTC (rev 128926)
+++ Products.PluggableAuthService/branches/maurits-login-transform/Products/PluggableAuthService/plugins/tests/test_ZODBUserManager.py	2012-12-28 15:33:06 UTC (rev 128927)
@@ -94,6 +94,70 @@
         self.assertRaises( KeyError, zum.addUser
                          , 'new_user', 'userid at example.com', '3733t' )
 
+    def test_addUser_lowercase( self ):
+
+        zum = self._makeOne()
+        # Transform login name to lowercase.
+        zum.login_transform =  'lower'
+
+        zum.addUser( 'Userid', ' Userid at Example.Com  ', 'password' )
+
+        user_ids = zum.listUserIds()
+        self.assertEqual( len( user_ids ), 1 )
+        self.assertEqual( user_ids[0], 'Userid' )
+        # No matter what we give as input, the login name is
+        # transformed to its canonical form before using it for
+        # searching.
+        self.assertEqual( zum.getUserIdForLogin( 'Userid at Example.Com' )
+                        , 'Userid' )
+        self.assertEqual( zum.getUserIdForLogin( 'USERID at EXAMPLE.COM' )
+                        , 'Userid' )
+        self.assertEqual( zum.getUserIdForLogin( 'userid at example.com' )
+                        , 'Userid' )
+        # We always return the canonical spelling.
+        self.assertEqual( zum.getLoginForUserId( 'Userid' )
+                        , 'userid at example.com' )
+        # We do not transform the user id.
+        self.assertRaises( KeyError, zum.getLoginForUserId, 'userid' )
+
+        info_list = zum.enumerateUsers()
+        self.assertEqual( len( info_list ), 1 )
+        info = info_list[ 0 ]
+        self.assertEqual( info[ 'id' ], 'Userid' )
+        self.assertEqual( info[ 'login' ], 'userid at example.com' )
+
+    def test_addUser_uppercase( self ):
+
+        zum = self._makeOne()
+        # Transform login name to uppercase.
+        zum.login_transform =  'upper'
+
+        zum.addUser( 'Userid', ' Userid at Example.Com  ', 'password' )
+
+        user_ids = zum.listUserIds()
+        self.assertEqual( len( user_ids ), 1 )
+        self.assertEqual( user_ids[0], 'Userid' )
+        # No matter what we give as input, the login name is
+        # transformed to its canonical form before using it for
+        # searching.
+        self.assertEqual( zum.getUserIdForLogin( 'Userid at Example.Com' )
+                        , 'Userid' )
+        self.assertEqual( zum.getUserIdForLogin( 'USERID at EXAMPLE.COM' )
+                        , 'Userid' )
+        self.assertEqual( zum.getUserIdForLogin( 'userid at example.com' )
+                        , 'Userid' )
+        # We always return the canonical spelling.
+        self.assertEqual( zum.getLoginForUserId( 'Userid' )
+                        , 'USERID at EXAMPLE.COM' )
+        # We do not transform the user id.
+        self.assertRaises( KeyError, zum.getLoginForUserId, 'USERID' )
+
+        info_list = zum.enumerateUsers()
+        self.assertEqual( len( info_list ), 1 )
+        info = info_list[ 0 ]
+        self.assertEqual( info[ 'id' ], 'Userid' )
+        self.assertEqual( info[ 'login' ], 'USERID at EXAMPLE.COM' )
+
     def test_removeUser_nonesuch( self ):
 
         zum = self._makeOne()
@@ -160,6 +224,33 @@
         self.assertEqual(zum.authenticateCredentials(
             { 'login' : 'userid' , 'password' : 'password'}), None)
 
+    def test_authenticateCredentials_lowercase( self ):
+
+        zum = self._makeOne()
+        zum.login_transform = 'lower'
+
+        zum.addUser( 'Userid', 'Userid at Example.Com', 'password' )
+
+        # The canonical login is lowercase.
+        user_id, login = zum.authenticateCredentials(
+                                { 'login' : 'userid at example.com'
+                                , 'password' : 'password'
+                                } )
+
+        self.assertEqual( user_id, 'Userid' )
+        self.assertEqual( login, 'userid at example.com' )
+
+        # We can still get authenticated when using the original or a
+        # different spelling, as long as the lowercase version is the
+        # same.  Also, whitespace is stripped before comparing.
+        user_id, login = zum.authenticateCredentials(
+                                { 'login' : '   Userid at Example.Com  '
+                                , 'password' : 'password'
+                                } )
+
+        self.assertEqual( user_id, 'Userid' )
+        self.assertEqual( login, 'userid at example.com' )
+
     def test_enumerateUsers_no_criteria( self ):
 
         from Products.PluggableAuthService.tests.test_PluggableAuthService \
@@ -379,6 +470,57 @@
             self.failUnless( info[ 'id' ] in SUBSET_IDS )
             self.failUnless( info[ 'login' ] in SUBSET_LOGINS )
 
+    def test_enumerateUsers_lowercase_logins( self ):
+
+        from Products.PluggableAuthService.tests.test_PluggableAuthService \
+            import FauxRoot
+
+        root = FauxRoot()
+        zum = self._makeOne( id='partial' ).__of__( root )
+        zum.login_transform = 'lower'
+
+        ID_LIST = ( 'Foo', 'Bar', 'Baz', 'Bam' )
+        LOGIN_LIST = [ '%s at example.com' % x for x in ID_LIST ]
+        CANONICAL_LOGIN_LIST = [ '%s at example.com' % x.lower() for x in ID_LIST ]
+
+        for i in range( len( ID_LIST ) ):
+
+            zum.addUser( ID_LIST[i], LOGIN_LIST[i], 'password' )
+
+        info_list = zum.enumerateUsers( login=CANONICAL_LOGIN_LIST )
+        self.assertEqual( len( info_list ), len( CANONICAL_LOGIN_LIST ) )
+        for info in info_list:
+            self.failUnless( info[ 'id' ] in ID_LIST )
+            self.failUnless( info[ 'login' ] in CANONICAL_LOGIN_LIST )
+
+        info_list = zum.enumerateUsers( login=LOGIN_LIST )
+        self.assertEqual( len( info_list ), len( LOGIN_LIST ) )
+        for info in info_list:
+            self.failUnless( info[ 'id' ] in ID_LIST )
+            self.failUnless( info[ 'login' ] in CANONICAL_LOGIN_LIST )
+            # If the next test fails, then the enumerateUsers code is
+            # changing the passed LOGIN_LIST, which is not good.
+            self.failUnless( info[ 'login' ] not in LOGIN_LIST )
+
+        SUBSET_LOGINS = LOGIN_LIST[:3]
+        CANONICAL_SUBSET_LOGINS = CANONICAL_LOGIN_LIST[:3]
+        SUBSET_IDS = ID_LIST[:3]
+
+        info_list = zum.enumerateUsers( login=CANONICAL_SUBSET_LOGINS )
+        self.assertEqual( len( info_list ), len( CANONICAL_SUBSET_LOGINS ) )
+        for info in info_list:
+            self.failUnless( info[ 'id' ] in SUBSET_IDS )
+            self.failUnless( info[ 'login' ] in CANONICAL_SUBSET_LOGINS )
+
+        info_list = zum.enumerateUsers( login=SUBSET_LOGINS )
+        self.assertEqual( len( info_list ), len( SUBSET_LOGINS ) )
+        for info in info_list:
+            self.failUnless( info[ 'id' ] in SUBSET_IDS )
+            self.failUnless( info[ 'login' ] in CANONICAL_SUBSET_LOGINS )
+            # If the next test fails, then the enumerateUsers code is
+            # changing the passed SUBSET_LOGINS, which is not good.
+            self.failUnless( info[ 'login' ] not in SUBSET_LOGINS )
+
     def test_authenticateWithOldPasswords( self ):
 
         try:
@@ -459,6 +601,35 @@
         self.assertEqual(user_id, 'user1')
         self.assertEqual(login, 'user1 at foobar.com')
 
+    def test_updateUser_lower(self):
+
+        zum = self._makeOne()
+        zum.login_transform = 'lower'
+
+        # Create a user and make sure we can authenticate with it
+        zum.addUser( 'User1', 'User1 at Example.Com', 'password' )
+        info1 = { 'login' : 'user1 at example.com', 'password' : 'password' }
+        user_id, login = zum.authenticateCredentials(info1)
+        self.assertEqual(user_id, 'User1')
+        self.assertEqual(login, 'user1 at example.com')
+
+        # Give the user a new login; attempts to authenticate with the
+        # old login must fail.
+        zum.updateUser('User1', 'User1 at Foobar.Com')
+        self.failIf(zum.authenticateCredentials(info1))
+
+        # Try to authenticate with the new login, this must succeed.
+        info2 = { 'login' : 'User1 at Foobar.Com', 'password' : 'password' }
+        user_id, login = zum.authenticateCredentials(info2)
+        self.assertEqual(user_id, 'User1')
+        self.assertEqual(login, 'user1 at foobar.com')
+
+        # Try to authenticate with the lowercase new login, this must succeed.
+        info3 = { 'login' : 'user1 at foobar.com', 'password' : 'password' }
+        user_id, login = zum.authenticateCredentials(info3)
+        self.assertEqual(user_id, 'User1')
+        self.assertEqual(login, 'user1 at foobar.com')
+
     def test_updateUser_login_name_conflicts(self):
         # See https://bugs.launchpad.net/zope-pas/+bug/789858
         zum = self._makeOne()
@@ -576,9 +747,12 @@
         # Test that a user can update her own password using the
         # ZMI-provided form handler: http://www.zope.org/Collectors/PAS/56
         zum = self._makeOne()
+        # Test lowercase logins immediately.  We could duplicate the
+        # test method too.
+        zum.login_transform = 'lower'
 
         # Create a user and make sure we can authenticate with it
-        zum.addUser( 'user1', 'user1 at example.com', 'password' )
+        zum.addUser( 'User1', 'User1 at Example.Com', 'password' )
         info1 = { 'login' : 'user1 at example.com', 'password' : 'password' }
         self.failUnless(zum.authenticateCredentials(info1))
 
@@ -597,9 +771,9 @@
         req.set('method', 'POST')
         req.SESSION = {'_csrft_': 'deadbeef'}
         req.form['csrf_token'] = 'deadbeef'
-        newSecurityManager(None, FauxUser('user1'))
+        newSecurityManager(None, FauxUser('User1'))
         try:
-            zum.manage_updatePassword('user2 at example.com',
+            zum.manage_updatePassword('User2 at Example.Com',
                                       'new_password',
                                       'new_password',
                                       REQUEST=req,
@@ -612,7 +786,7 @@
         # Try to authenticate with the new password, this must succeed.
         info2 = { 'login' : 'user2 at example.com', 'password' : 'new_password' }
         user_id, login = zum.authenticateCredentials(info2)
-        self.assertEqual(user_id, 'user1')
+        self.assertEqual(user_id, 'User1')
         self.assertEqual(login, 'user2 at example.com')
 
     def test_manage_updateUserPassword_POST_permissions(self):
@@ -706,7 +880,33 @@
         req.SESSION['_csrft_'] = 'deadbeef'
         zum.manage_removeUsers([USER_ID], REQUEST=req)
 
+    def test_applyTransform( self ):
 
+        zum = self._makeOne()
+        self.assertEqual(zum.applyTransform(' User '), ' User ')
+        zum.login_transform =  'lower'
+        self.assertEqual(zum.applyTransform(' User '), 'user')
+        self.assertEqual(zum.applyTransform(u' User '), u'user')
+        self.assertEqual(zum.applyTransform(''), '')
+        self.assertEqual(zum.applyTransform(None), None)
+        self.assertRaises(AttributeError, zum.applyTransform, 123)
+        self.assertRaises(AttributeError, zum.applyTransform, ['User'])
+        zum.login_transform =  'upper'
+        self.assertEqual(zum.applyTransform(' User '), 'USER')
+        # Let's not fail just because a user has accidentally left a
+        # space at the end of the login_transform name.  That could
+        # lead to hard-to-debug behaviour.
+        zum.login_transform =  ' upper  '
+        self.assertEqual(zum.applyTransform(' User '), 'USER')
+        # We would want to test what happens when the login_transform
+        # attribute is not there, but the following only removes it
+        # from the instance, not the class.  Oh well.
+        del zum.login_transform
+        self.assertEqual(zum.applyTransform(' User '), ' User ')
+        zum.login_transform =  'nonexisting'
+        self.assertEqual(zum.applyTransform(' User '), ' User ')
+
+
 def test_suite():
     return unittest.TestSuite((
         unittest.makeSuite( ZODBUserManagerTests ),



More information about the checkins mailing list