Merge lp:~jml/launchpad/more-login-helpers into lp:launchpad

Proposed by Jonathan Lange
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 11122
Proposed branch: lp:~jml/launchpad/more-login-helpers
Merge into: lp:launchpad
Diff against target: 411 lines (+278/-18)
6 files modified
lib/lp/code/model/tests/test_codeimportjob.py (+4/-5)
lib/lp/testing/__init__.py (+5/-4)
lib/lp/testing/_login.py (+73/-7)
lib/lp/testing/tests/test_login.py (+191/-0)
lib/lp/translations/tests/test_hastranslationtemplates.py (+3/-1)
lib/lp/translations/tests/test_pofile.py (+2/-1)
To merge this branch: bzr merge lp:~jml/launchpad/more-login-helpers
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
Review via email: mp+29595@code.launchpad.net

Commit message

Add login_team, login_as and login_celebrity helpers.

Description of the change

This branch adds a few new login test helpers: login_team, login_as and login_celebrity.

login_team logs you in as an arbitrary member of a team. login_as logs you in as pretty much whatever you give it: None, ANONYMOUS, a person or a team. login_celebrity will log you in as whatever celebrity you provide.

This branch also has another big advantage: tests! For the first time ever, the login helpers themselves have unit tests.

I did some drive-by cleanup in lp/testing/__init__ too, getting rid of an unnecessary re-export.

Probably the weirdest thing about this branch is the way I figure out the currently logged in user. As far as I can tell, we have two mechanisms: inspecting the zope security policy and checking the launch bag. I've used both to be extra safe, but would happily be convinced to use only one.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

+ ANONYMOUS are equivalent, and will log the person is as the anonymous

typo - ITYM 'in as'

get_arbitrary should return different members each time I think. At the moment it will give a false sense of arbitrariness.

If you don't like random(), perhaps walking the team list from end to start would do (so that the owner isn't always the first one returned).

Rather than an XXX, I think you've just written some clear explanation of a bit of cruft you're tolerating - thats fine. As I understand XXX's in the lp code base they should be attached to something to fix - and in this case I'd put it on the 'launch bag' saying 'this duplicates the security context and should be deleted'.

Finally, I'd really like a single class rather than these separate functions, but that would be future work, its not needed for this patch - this patch is a clear improvement on its own.

-Rob

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/model/tests/test_codeimportjob.py'
2--- lib/lp/code/model/tests/test_codeimportjob.py 2010-05-14 01:46:38 +0000
3+++ lib/lp/code/model/tests/test_codeimportjob.py 2010-07-13 10:01:07 +0000
4@@ -36,14 +36,14 @@
5 ICodeImportJobSet, ICodeImportJobWorkflow)
6 from lp.code.interfaces.codeimportresult import ICodeImportResult
7 from lp.registry.interfaces.person import IPersonSet
8-from lp.testing import ANONYMOUS, login, logout, TestCaseWithFactory
9+from lp.testing import (
10+ ANONYMOUS, login, login_celebrity, logout, TestCaseWithFactory)
11 from canonical.launchpad.testing.codeimporthelpers import (
12 make_finished_import, make_running_import)
13 from canonical.launchpad.testing.pages import get_feedback_messages
14 from canonical.launchpad.webapp import canonical_url
15 from canonical.librarian.interfaces import ILibrarianClient
16-from canonical.testing import (
17- LaunchpadFunctionalLayer, LaunchpadZopelessLayer)
18+from canonical.testing import LaunchpadFunctionalLayer
19
20
21 def login_for_code_imports():
22@@ -52,8 +52,7 @@
23 CodeImports are currently hidden from regular users currently. Members of
24 the vcs-imports team and can access the objects freely.
25 """
26- # David Allouche is a member of the vcs-imports team.
27- login('david.allouche@canonical.com')
28+ login_celebrity('vcs_imports')
29
30
31 class TestCodeImportJobSet(unittest.TestCase):
32
33=== modified file 'lib/lp/testing/__init__.py'
34--- lib/lp/testing/__init__.py 2010-06-28 20:30:32 +0000
35+++ lib/lp/testing/__init__.py 2010-07-13 10:01:07 +0000
36@@ -19,7 +19,10 @@
37 'launchpadlib_for',
38 'launchpadlib_credentials_for',
39 'login',
40+ 'login_as',
41+ 'login_celebrity',
42 'login_person',
43+ 'login_team',
44 'logout',
45 'map_branch_contents',
46 'normalize_whitespace',
47@@ -33,9 +36,6 @@
48 'test_tales',
49 'time_counter',
50 'unlink_source_packages',
51- # XXX: This really shouldn't be exported from here. People should import
52- # it from Zope.
53- 'verifyObject',
54 'validate_mock_class',
55 'WindmillTestCase',
56 'with_anonymous_login',
57@@ -92,7 +92,8 @@
58 # Import the login and logout functions here as it is a much better
59 # place to import them from in tests.
60 from lp.testing._login import (
61- is_logged_in, login, login_person, logout)
62+ is_logged_in, login, login_as, login_celebrity, login_person, login_team,
63+ logout)
64 # canonical.launchpad.ftests expects test_tales to be imported from here.
65 # XXX: JonathanLange 2010-01-01: Why?!
66 from lp.testing._tales import test_tales
67
68=== modified file 'lib/lp/testing/_login.py'
69--- lib/lp/testing/_login.py 2010-04-15 21:07:53 +0000
70+++ lib/lp/testing/_login.py 2010-07-13 10:01:07 +0000
71@@ -1,21 +1,32 @@
72-# Copyright 2009 Canonical Ltd. This software is licensed under the
73+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
74 # GNU Affero General Public License version 3 (see the file LICENSE).
75
76 # We like global statements!
77 # pylint: disable-msg=W0602,W0603
78 __metaclass__ = type
79
80+__all__ = [
81+ 'login',
82+ 'login_as',
83+ 'login_celebrity',
84+ 'login_person',
85+ 'login_team',
86+ 'logout',
87+ 'is_logged_in',
88+ ]
89+
90+import random
91+
92+from zope.component import getUtility
93 from zope.security.management import endInteraction
94+from zope.security.proxy import removeSecurityProxy
95+
96+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
97 from canonical.launchpad.webapp.interaction import (
98- setupInteractionByEmail, setupInteractionForPerson)
99+ ANONYMOUS, setupInteractionByEmail, setupInteractionForPerson)
100 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
101 from canonical.launchpad.webapp.vhosts import allvhosts
102
103-__all__ = [
104- 'login',
105- 'login_person',
106- 'logout',
107- 'is_logged_in']
108
109
110 _logged_in = False
111@@ -61,10 +72,65 @@
112
113 def login_person(person, participation=None):
114 """Login the person with their preferred email."""
115+ if person is not None:
116+ # The login will fail even without this check, but this gives us a
117+ # nice error message, which can save time when debugging.
118+ if getattr(person, 'is_team', None):
119+ raise ValueError("Got team, expected person: %r" % (person,))
120 participation = _test_login_impl(participation)
121 setupInteractionForPerson(person, participation)
122
123
124+def _get_arbitrary_team_member(team):
125+ """Get an arbitrary member of 'team'.
126+
127+ :param team: An `ITeam`.
128+ """
129+ # Set up the interaction.
130+ login(ANONYMOUS)
131+ return random.choice(list(team.allmembers))
132+
133+
134+def login_team(team, participation=None):
135+ """Login as a member of 'team'."""
136+ # This check isn't strictly necessary (it depends on the implementation of
137+ # _get_arbitrary_team_member), but this gives us a nice error message,
138+ # which can save time when debugging.
139+ if not team.is_team:
140+ raise ValueError("Got person, expected team: %r" % (team,))
141+ person = _get_arbitrary_team_member(team)
142+ login_person(person, participation=participation)
143+ return person
144+
145+
146+def login_as(person_or_team, participation=None):
147+ """Login as a person or a team.
148+
149+ :param person_or_team: A person, a team, ANONYMOUS or None. None and
150+ ANONYMOUS are equivalent, and will log the person in as the anonymous
151+ user.
152+ """
153+ if person_or_team == ANONYMOUS:
154+ login_method = login
155+ elif person_or_team is None:
156+ login_method = login_person
157+ elif person_or_team.is_team:
158+ login_method = login_team
159+ else:
160+ login_method = login_person
161+ return login_method(person_or_team, participation=participation)
162+
163+
164+def login_celebrity(celebrity_name, participation=None):
165+ """Login as a celebrity."""
166+ login(ANONYMOUS)
167+ celebs = getUtility(ILaunchpadCelebrities)
168+ celeb = getattr(celebs, celebrity_name, None)
169+ if celeb is None:
170+ raise ValueError("No such celebrity: %r" % (celebrity_name,))
171+ return login_as(celeb, participation=participation)
172+
173+
174 def logout():
175 """Tear down after login(...), ending the current interaction.
176
177
178=== added file 'lib/lp/testing/tests/test_login.py'
179--- lib/lp/testing/tests/test_login.py 1970-01-01 00:00:00 +0000
180+++ lib/lp/testing/tests/test_login.py 2010-07-13 10:01:07 +0000
181@@ -0,0 +1,191 @@
182+# Copyright 2010 Canonical Ltd. This software is licensed under the
183+# GNU Affero General Public License version 3 (see the file LICENSE).
184+
185+"""Tests for the login helpers."""
186+
187+__metaclass__ = type
188+
189+import unittest
190+
191+from zope.app.security.interfaces import IUnauthenticatedPrincipal
192+from zope.component import getUtility
193+
194+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
195+from canonical.launchpad.webapp.interaction import get_current_principal
196+from canonical.launchpad.webapp.interfaces import IOpenLaunchBag
197+from canonical.testing.layers import DatabaseFunctionalLayer
198+from lp.testing import (
199+ ANONYMOUS, is_logged_in, login, login_as, login_celebrity, login_person,
200+ login_team, logout)
201+from lp.testing import TestCaseWithFactory
202+
203+
204+class TestLoginHelpers(TestCaseWithFactory):
205+
206+ layer = DatabaseFunctionalLayer
207+
208+ def getLoggedInPerson(self):
209+ """Return the currently logged-in person.
210+
211+ If no one is logged in, return None. If there is an anonymous user
212+ logged in, then return ANONYMOUS. Otherwise, return the logged-in
213+ `IPerson`.
214+ """
215+ # I don't really know the canonical way of asking for "the logged-in
216+ # person", so instead I'm using all the ways I can find and making
217+ # sure they match each other. -- jml
218+ by_launchbag = getUtility(IOpenLaunchBag).user
219+ principal = get_current_principal()
220+ if principal is None:
221+ return None
222+ elif IUnauthenticatedPrincipal.providedBy(principal):
223+ if by_launchbag is None:
224+ return ANONYMOUS
225+ else:
226+ raise ValueError(
227+ "Unauthenticated principal, but launchbag thinks "
228+ "%r is logged in." % (by_launchbag,))
229+ else:
230+ by_principal = principal.person
231+ self.assertEqual(by_launchbag, by_principal)
232+ return by_principal
233+
234+ def assertLoggedIn(self, person):
235+ """Assert that 'person' is logged in."""
236+ self.assertEqual(person, self.getLoggedInPerson())
237+
238+ def assertLoggedOut(self):
239+ """Assert that no one is currently logged in."""
240+ self.assertIs(None, get_current_principal())
241+ self.assertIs(None, getUtility(IOpenLaunchBag).user)
242+
243+ def test_not_logged_in(self):
244+ # After logout has been called, we are not logged in.
245+ logout()
246+ self.assertEqual(False, is_logged_in())
247+ self.assertLoggedOut()
248+
249+ def test_logout_twice(self):
250+ # Logging out twice don't harm anybody none.
251+ logout()
252+ logout()
253+ self.assertEqual(False, is_logged_in())
254+ self.assertLoggedOut()
255+
256+ def test_logged_in(self):
257+ # After login has been called, we are logged in.
258+ login_person(self.factory.makePerson())
259+ self.assertEqual(True, is_logged_in())
260+
261+ def test_login_person_actually_logs_in(self):
262+ # login_person changes the currently logged in person.
263+ person = self.factory.makePerson()
264+ logout()
265+ login_person(person)
266+ self.assertLoggedIn(person)
267+
268+ def test_login_different_person_overrides(self):
269+ # Calling login_person a second time with a different person changes
270+ # the currently logged in user.
271+ a = self.factory.makePerson()
272+ b = self.factory.makePerson()
273+ logout()
274+ login_person(a)
275+ login_person(b)
276+ self.assertLoggedIn(b)
277+
278+ def test_login_person_with_team(self):
279+ # Calling login_person with a team raises a nice error.
280+ team = self.factory.makeTeam()
281+ e = self.assertRaises(ValueError, login_person, team)
282+ self.assertEqual(str(e), "Got team, expected person: %r" % (team,))
283+
284+ def test_login_account(self):
285+ # Calling login_person with an account logs you in with that account.
286+ person = self.factory.makePerson()
287+ account = person.account
288+ login_person(account)
289+ self.assertLoggedIn(person)
290+
291+ def test_login_with_email(self):
292+ # login() logs a person in by email.
293+ person = self.factory.makePerson()
294+ email = person.preferredemail.email
295+ logout()
296+ login(email)
297+ self.assertLoggedIn(person)
298+
299+ def test_login_anonymous(self):
300+ # login as 'ANONYMOUS' logs in as the anonymous user.
301+ logout()
302+ login(ANONYMOUS)
303+ self.assertLoggedIn(ANONYMOUS)
304+
305+ def test_login_team(self):
306+ # login_team() logs in as a member of the given team.
307+ team = self.factory.makeTeam()
308+ logout()
309+ login_team(team)
310+ person = self.getLoggedInPerson()
311+ self.assertTrue(person.inTeam(team))
312+
313+ def test_login_team_with_person(self):
314+ # Calling login_team() with a person instead of a team raises a nice
315+ # error.
316+ person = self.factory.makePerson()
317+ logout()
318+ e = self.assertRaises(ValueError, login_team, person)
319+ self.assertEqual(str(e), "Got person, expected team: %r" % (person,))
320+
321+ def test_login_team_returns_logged_in_person(self):
322+ # login_team returns the logged-in person.
323+ team = self.factory.makeTeam()
324+ logout()
325+ person = login_team(team)
326+ self.assertLoggedIn(person)
327+
328+ def test_login_as_person(self):
329+ # login_as() logs in as a person if it's given a person.
330+ person = self.factory.makePerson()
331+ logout()
332+ login_as(person)
333+ self.assertLoggedIn(person)
334+
335+ def test_login_as_team(self):
336+ # login_as() logs in as a member of a team if it's given a team.
337+ team = self.factory.makeTeam()
338+ logout()
339+ login_as(team)
340+ person = self.getLoggedInPerson()
341+ self.assertTrue(person.inTeam(team))
342+
343+ def test_login_as_anonymous(self):
344+ # login_as(ANONYMOUS) logs in as the anonymous user.
345+ logout()
346+ login_as(ANONYMOUS)
347+ self.assertLoggedIn(ANONYMOUS)
348+
349+ def test_login_as_None(self):
350+ # login_as(None) logs in as the anonymous user.
351+ logout()
352+ login_as(None)
353+ self.assertLoggedIn(ANONYMOUS)
354+
355+ def test_login_celebrity(self):
356+ # login_celebrity logs in a celebrity.
357+ logout()
358+ login_celebrity('vcs_imports')
359+ vcs_imports = getUtility(ILaunchpadCelebrities).vcs_imports
360+ person = self.getLoggedInPerson()
361+ self.assertTrue(person.inTeam, vcs_imports)
362+
363+ def test_login_nonexistent_celebrity(self):
364+ # login_celebrity raises ValueError when called with a non-existent
365+ # celebrity.
366+ logout()
367+ e = self.assertRaises(ValueError, login_celebrity, 'nonexistent')
368+ self.assertEqual(str(e), "No such celebrity: 'nonexistent'")
369+
370+
371+def test_suite():
372+ return unittest.TestLoader().loadTestsFromName(__name__)
373
374=== modified file 'lib/lp/translations/tests/test_hastranslationtemplates.py'
375--- lib/lp/translations/tests/test_hastranslationtemplates.py 2009-07-17 02:25:09 +0000
376+++ lib/lp/translations/tests/test_hastranslationtemplates.py 2010-07-13 10:01:07 +0000
377@@ -5,11 +5,13 @@
378
379 import unittest
380
381+from zope.interface.verify import verifyObject
382+
383 from canonical.testing import ZopelessDatabaseLayer
384 from lp.translations.interfaces.potemplate import IHasTranslationTemplates
385 from lp.translations.interfaces.translationfileformat import (
386 TranslationFileFormat)
387-from lp.testing import TestCaseWithFactory, verifyObject
388+from lp.testing import TestCaseWithFactory
389
390
391 class HasTranslationTemplatesTestMixin(TestCaseWithFactory):
392
393=== modified file 'lib/lp/translations/tests/test_pofile.py'
394--- lib/lp/translations/tests/test_pofile.py 2010-04-23 14:46:43 +0000
395+++ lib/lp/translations/tests/test_pofile.py 2010-07-13 10:01:07 +0000
396@@ -11,6 +11,7 @@
397 from unittest import TestLoader
398
399 from zope.component import getAdapter, getUtility
400+from zope.interface.verify import verifyObject
401 from zope.security.proxy import removeSecurityProxy
402
403 from lp.translations.interfaces.pofile import IPOFileSet
404@@ -20,7 +21,7 @@
405 TranslationValidationStatus)
406 from lp.translations.interfaces.translationcommonformat import (
407 ITranslationFileData)
408-from lp.testing import TestCaseWithFactory, verifyObject
409+from lp.testing import TestCaseWithFactory
410 from canonical.testing import LaunchpadZopelessLayer, ZopelessDatabaseLayer
411 from canonical.launchpad.webapp.publisher import canonical_url
412