Repository: buildbot/buildbot

Issue:
LdapUserInfo  Avatar picture broken
Since I cannot find any reference to my custom code I assume it is an error in buildbot?

```
	Traceback (most recent call last):
	  File "python3.8/site-packages/buildbot/www/avatar.py", line 79, in render_GET
	    return self.asyncRenderHelper(request, self.renderAvatar)
	  File "python3.8/site-packages/buildbot/www/resource.py", line 72, in asyncRenderHelper
	    d = _callable(request)
	  File "python3.8/site-packages/twisted/internet/defer.py", line 1613, in unwindGenerator
	    return _cancellableInlineCallbacks(gen)
	  File "python3.8/site-packages/twisted/internet/defer.py", line 1529, in _cancellableInlineCallbacks
	    _inlineCallbacks(None, g, status)
	--- <exception caught here> ---
	  File "python3.8/site-packages/twisted/internet/defer.py", line 654, in _runCallbacks
	    current.result = callback(current.result, *args, **kw)
	  File "python3.8/site-packages/buildbot/www/resource.py", line 96, in failHttpError
	    f.trap(Error)
	  File "python3.8/site-packages/twisted/python/failure.py", line 460, in trap
	    self.raiseException()
	  File "python3.8/site-packages/twisted/python/failure.py", line 488, in raiseException
	    raise self.value.with_traceback(self.tb)
	  File "python3.8/site-packages/twisted/internet/defer.py", line 654, in _runCallbacks
	    current.result = callback(current.result, *args, **kw)
	  File "python3.8/site-packages/buildbot/www/resource.py", line 89, in failHttpRedirect
	    f.trap(Redirect)
	  File "python3.8/site-packages/twisted/python/failure.py", line 460, in trap
	    self.raiseException()
	  File "python3.8/site-packages/twisted/python/failure.py", line 488, in raiseException
	    raise self.value.with_traceback(self.tb)
	  File "python3.8/site-packages/twisted/internet/defer.py", line 1418, in _inlineCallbacks
	    result = g.send(result)
	  File "python3.8/site-packages/buildbot/www/avatar.py", line 91, in renderAvatar
	    res = yield method.getUserAvatar(email, username, size, self.defaultAvatarFullUrl)
	builtins.TypeError: getUserAvatar() takes 4 positional arguments but 5 were given
```

Buildbot master version: 2.10.1
buildbot worker version: 2020.08.31
Python version: 3.8.5

avatar plugin: buildbot.plugins.util.LdapUserInfo


Discussion:
Helo,

Can you tell us which avatar plugin are you using?

buildbot.plugins.util.LdapUserInfo
There has been a PR recently which improved the avatar api
https://github.com/buildbot/buildbot/pull/5753

it looks like the LDAPUserInfo one was not updated.

Can you have a look at it, it is probably just adding the additional parameter to the method. (and ignore it)
No, because I did not break it.
@PhilippSelenium I a sorry about this feedback...

We rely on the community to maintain features they care about.
I'm sorry too, but I just do not have the time and it does not yet bug me enough to fix it.
@lephilousophe Any chance you are willing to fix the leftovers of your PR?
Hi there,
I don't know why but I got the notification only today.

Sorry I overlooked the LDAPUserInfo.
I will look at it this evening to fix it.
For the record, after a little investigation problem was caused by #5420.
My PR hasn't landed in 2.10 branch.
Will submit a fix PR anyway.

Resolving patch:
diff --git a/master/buildbot/www/ldapuserinfo.py b/master/buildbot/www/ldapuserinfo.py
--- a/master/buildbot/www/ldapuserinfo.py
+++ b/master/buildbot/www/ldapuserinfo.py
@@ -138,20 +138,28 @@ def getFirstLdapInfo(x):
     def findAvatarMime(self, data):
         # http://en.wikipedia.org/wiki/List_of_file_signatures
         if data.startswith(b"\xff\xd8\xff"):
-            return ("image/jpeg", data)
+            return (b"image/jpeg", data)
         if data.startswith(b"\x89PNG"):
-            return ("image/png", data)
+            return (b"image/png", data)
         if data.startswith(b"GIF8"):
-            return ("image/gif", data)
+            return (b"image/gif", data)
         # ignore unknown image format
         return None
 
-    def getUserAvatar(self, user_email, size, defaultAvatarUrl):
-        user_email = bytes2unicode(user_email)
+    def getUserAvatar(self, email, username, size, defaultAvatarUrl):
+        if username:
+            username = bytes2unicode(username)
+        if email:
+            email = bytes2unicode(email)
 
         def thd():
             c = self.connectLdap()
-            pattern = self.avatarPattern % dict(email=user_email)
+            if username:
+                pattern = self.accountPattern % dict(username=username)
+            elif email:
+                pattern = self.avatarPattern % dict(email=email)
+            else:
+                return None
             res = self.search(c, self.accountBase, pattern,
                               attributes=[self.avatarData])
             if not res:


Test patch:
diff --git a/master/buildbot/test/unit/www/test_ldapuserinfo.py b/master/buildbot/test/unit/www/test_ldapuserinfo.py
--- a/master/buildbot/test/unit/www/test_ldapuserinfo.py
+++ b/master/buildbot/test/unit/www/test_ldapuserinfo.py
@@ -21,6 +21,9 @@
 from twisted.internet import defer
 from twisted.trial import unittest
 
+from buildbot.test.util.misc import TestReactorMixin
+from buildbot.test.util.www import WwwTestMixin
+from buildbot.www import avatar
 from buildbot.www import ldapuserinfo
 
 
@@ -152,40 +155,94 @@ def test_updateUserInfoGroupsUnicodeDn(self):
         self.assertEqual(res, {'email': 'mee@too', 'full_name': 'me too',
                                'groups': ["group", "group2"], 'username': 'me'})
 
+
+class LdapAvatar(CommonTestCase, TestReactorMixin, WwwTestMixin):
+    @defer.inlineCallbacks
+    def setUp(self):
+        CommonTestCase.setUp(self)
+        self.setUpTestReactor()
+
+        master = self.make_master(
+            url='http://a/b/',
+            avatar_methods=[self.userInfoProvider])
+
+        self.rsrc = avatar.AvatarResource(master)
+        self.rsrc.reconfigResource(master.config)
+
+        yield self.master.startService()
+
+    def makeUserInfoProvider(self):
+        self.userInfoProvider = ldapuserinfo.LdapUserInfo(
+            uri="ldap://uri", bindUser="user", bindPw="pass",
+            accountBase="accbase", groupBase="groupbase",
+            accountPattern="accpattern=%(username)s", groupMemberPattern="groupMemberPattern",
+            accountFullName="accountFullName",
+            accountEmail="accountEmail",
+            groupName="groupName",
+            avatarPattern="avatar=%(email)s",
+            avatarData="picture",
+            accountExtraFields=["myfield"])
+
     @defer.inlineCallbacks
     def _getUserAvatar(self, mimeTypeAndData):
         (mimeType, data) = mimeTypeAndData
         self.makeRawSearchSideEffect([
             [("cn", {"picture": [data]})]])
-        res = yield self.userInfoProvider.getUserAvatar("me", 21, None)
+        res = yield self.render_resource(self.rsrc, b'/?email=me')
         self.assertSearchCalledWith([
-            (('accbase', 'avatar', ['picture']), {}),
+            (('accbase', 'avatar=me', ['picture']), {}),
         ])
         return res
 
     @defer.inlineCallbacks
     def test_getUserAvatarPNG(self):
-        mimeTypeAndData = ('image/png', b'\x89PNG lljklj')
-        res = yield self._getUserAvatar(mimeTypeAndData)
-        self.assertEqual(res, mimeTypeAndData)
+        mimeTypeAndData = (b'image/png', b'\x89PNG lljklj')
+        yield self._getUserAvatar(mimeTypeAndData)
+        self.assertRequest(contentType=mimeTypeAndData[0],
+            content=mimeTypeAndData[1])
 
     @defer.inlineCallbacks
     def test_getUserAvatarJPEG(self):
-        mimeTypeAndData = ('image/jpeg', b'\xff\xd8\xff lljklj')
-        res = yield self._getUserAvatar(mimeTypeAndData)
-        self.assertEqual(res, mimeTypeAndData)
+        mimeTypeAndData = (b'image/jpeg', b'\xff\xd8\xff lljklj')
+        yield self._getUserAvatar(mimeTypeAndData)
+        self.assertRequest(contentType=mimeTypeAndData[0],
+            content=mimeTypeAndData[1])
 
     @defer.inlineCallbacks
     def test_getUserAvatarGIF(self):
-        mimeTypeAndData = ('image/gif', b'GIF8 lljklj')
-        res = yield self._getUserAvatar(mimeTypeAndData)
-        self.assertEqual(res, mimeTypeAndData)
+        mimeTypeAndData = (b'image/gif', b'GIF8 lljklj')
+        yield self._getUserAvatar(mimeTypeAndData)
+        self.assertRequest(contentType=mimeTypeAndData[0],
+            content=mimeTypeAndData[1])
 
     @defer.inlineCallbacks
     def test_getUserAvatarUnknownType(self):
-        mimeTypeAndData = ('', b'unknown image format')
+        mimeTypeAndData = (b'', b'unknown image format')
         res = yield self._getUserAvatar(mimeTypeAndData)
-        self.assertIsNone(res)
+        # Unknown format means data won't be sent
+        self.assertEqual(res, dict(redirected=b'img/nobody.png'))
+
+    @defer.inlineCallbacks
+    def test_getUsernameAvatar(self):
+        mimeType = b'image/gif'
+        data = b'GIF8 lljklj'
+        self.makeRawSearchSideEffect([
+            [("cn", {"picture": [data]})]])
+        yield self.render_resource(self.rsrc, b'/?username=me')
+        self.assertSearchCalledWith([
+            (('accbase', 'accpattern=me', ['picture']), {}),
+        ])
+        self.assertRequest(contentType=mimeType,
+            content=data)
+
+    @defer.inlineCallbacks
+    def test_getUnknownUsernameAvatar(self):
+        self.makeSearchSideEffect([[], [], []])
+        res = yield self.render_resource(self.rsrc, b'/?username=other')
+        self.assertSearchCalledWith([
+            (('accbase', 'accpattern=other', ['picture']), {}),
+        ])
+        self.assertEqual(res, dict(redirected=b'img/nobody.png'))
 
 
 class LdapUserInfoNotEscCharsDn(CommonTestCase):