Merge lp:~leonardr/launchpad/launchpadlib-pagetests-take-2 into lp:launchpad/db-devel
- launchpadlib-pagetests-take-2
- Merge into db-devel
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~leonardr/launchpad/launchpadlib-pagetests-take-2 |
Merge into: | lp:launchpad/db-devel |
Diff against target: |
615 lines (+340/-92) 12 files modified
Makefile (+0/-4) lib/canonical/buildd/debian/control (+1/-1) lib/canonical/launchpad/apidoc/wadl-testrunner-devel.xml (+0/-10) lib/canonical/launchpad/browser/oauth.py (+25/-13) lib/canonical/launchpad/doc/oauth.txt (+54/-0) lib/canonical/launchpad/pagetests/webservice/launchpadlib.txt (+50/-0) lib/canonical/launchpad/pagetests/webservice/xx-wadl.txt (+68/-59) lib/canonical/launchpad/systemhomes.py (+9/-4) lib/canonical/launchpad/testing/pages.py (+5/-1) lib/canonical/testing/layers.py (+7/-0) lib/lp/testing/__init__.py (+2/-0) lib/lp/testing/_webservice.py (+119/-0) |
To merge this branch: | bzr merge lp:~leonardr/launchpad/launchpadlib-pagetests-take-2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Guilherme Salgado (community) | code | Approve | |
Michael Hudson-Doyle | Approve | ||
Review via email: mp+22444@code.launchpad.net |
Commit message
Description of the change
This is the second version of my branch to make it possible to use launchpadlib in pagetests. The previous branch was landed and then removed because it caused mysterious, catastrophic test failures (though not on my dev machine, on ec2, or on buildout).
The previous branch put a 'launchpad' object in the globally accessible globs. This branch instead makes available a 'launchpadlib_for' helper function that will create a Launchpad object for any user, creating the underlying OAuth credential if necessary. Most of the work happens in two more granular helper methods, launchpadlib_
I refactored the code that turns a string into an OAuth context object into a helper function, so that oauth_access_
Everything else in this branch (eg. the change to the WADL code) is exactly the same as in the previous launchpadlib pagetest branch.
Leonard Richardson (leonardr) wrote : | # |
Michael Hudson-Doyle (mwhudson) wrote : | # |
I don't understand the changes to xx-wadl.txt but they've already been reviewed once already, right?
The rest looks fine.
Leonard Richardson (leonardr) wrote : | # |
ec2 test turned up some test failures. Two of them were trivial to fix; one was quite difficult and revealed that Launchpad doesn't really support a feature we thought we'd implemented. I filed bug 552732 to cover that and left the test in place but commented out. Incremental diff:
Guilherme Salgado (salgado) wrote : | # |
Per Leonard's request I've reviewed http://
Preview Diff
1 | === modified file 'Makefile' |
2 | --- Makefile 2010-03-26 13:23:20 +0000 |
3 | +++ Makefile 2010-04-05 16:09:33 +0000 |
4 | @@ -324,11 +324,7 @@ |
5 | $(RM) -r lib/mailman |
6 | $(RM) -rf lib/canonical/launchpad/icing/build/* |
7 | $(RM) -r $(CODEHOSTING_ROOT) |
8 | - mv $(APIDOC_DIR)/wadl-testrunner-devel.xml \ |
9 | - $(APIDOC_DIR)/wadl-testrunner-devel.xml.bak |
10 | $(RM) $(APIDOC_DIR)/wadl*.xml $(APIDOC_DIR)/*.html |
11 | - mv $(APIDOC_DIR)/wadl-testrunner-devel.xml.bak \ |
12 | - $(APIDOC_DIR)/wadl-testrunner-devel.xml |
13 | $(RM) -rf $(APIDOC_DIR).tmp |
14 | $(RM) $(BZR_VERSION_INFO) |
15 | $(RM) _pythonpath.py |
16 | |
17 | === modified file 'lib/canonical/buildd/debian/control' |
18 | --- lib/canonical/buildd/debian/control 2009-11-17 22:28:43 +0000 |
19 | +++ lib/canonical/buildd/debian/control 2010-04-05 16:09:33 +0000 |
20 | @@ -8,7 +8,7 @@ |
21 | Package: launchpad-buildd |
22 | Section: misc |
23 | Architecture: all |
24 | -Depends: python-twisted, debootstrap, dpkg-dev, linux32, file, bzip2, sudo, ntpdate, adduser, apt-transport-https |
25 | +Depends: python-twisted-core, python-twisted-web, debootstrap, dpkg-dev, linux32, file, bzip2, sudo, ntpdate, adduser, apt-transport-https |
26 | Description: Launchpad buildd slave |
27 | This is the launchpad buildd slave package. It contains everything needed to |
28 | get a launchpad buildd going apart from the database manipulation required to |
29 | |
30 | === added directory 'lib/canonical/launchpad/apidoc' |
31 | === removed directory 'lib/canonical/launchpad/apidoc' |
32 | === removed file 'lib/canonical/launchpad/apidoc/wadl-testrunner-devel.xml' |
33 | --- lib/canonical/launchpad/apidoc/wadl-testrunner-devel.xml 2010-03-26 23:01:30 +0000 |
34 | +++ lib/canonical/launchpad/apidoc/wadl-testrunner-devel.xml 1970-01-01 00:00:00 +0000 |
35 | @@ -1,10 +0,0 @@ |
36 | -<?xml version="1.0"?> |
37 | -<wadl:application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
38 | - xmlns="http://research.sun.com/wadl/2006/10" |
39 | - xmlns:wadl="http://research.sun.com/wadl/2006/10" |
40 | - xsi:schemaLocation="http://research.sun.com/wadl/2006/10/wadl.xsd"> |
41 | - |
42 | - <!--This file is for testing purposes only. See |
43 | - canonical/launcpad/pagetests/webservice/xx-wadl.txt --> |
44 | - |
45 | -</wadl:application> |
46 | |
47 | === modified file 'lib/canonical/launchpad/browser/oauth.py' |
48 | --- lib/canonical/launchpad/browser/oauth.py 2009-07-17 00:26:05 +0000 |
49 | +++ lib/canonical/launchpad/browser/oauth.py 2010-04-05 16:09:33 +0000 |
50 | @@ -6,7 +6,8 @@ |
51 | 'OAuthAccessTokenView', |
52 | 'OAuthAuthorizeTokenView', |
53 | 'OAuthRequestTokenView', |
54 | - 'OAuthTokenAuthorizedView'] |
55 | + 'OAuthTokenAuthorizedView', |
56 | + 'lookup_oauth_context'] |
57 | |
58 | import simplejson |
59 | |
60 | @@ -172,18 +173,10 @@ |
61 | context = self.request.form.get('lp.context') |
62 | if not context: |
63 | return |
64 | - if '/' in context: |
65 | - distro, package = context.split('/') |
66 | - distro = getUtility(IDistributionSet).getByName(distro) |
67 | - if distro is None: |
68 | - raise UnexpectedFormData("Unknown context.") |
69 | - context = distro.getSourcePackage(package) |
70 | - if context is None: |
71 | - raise UnexpectedFormData("Unknown context.") |
72 | - else: |
73 | - context = getUtility(IPillarNameSet).getByName(context) |
74 | - if context is None: |
75 | - raise UnexpectedFormData("Unknown context.") |
76 | + try: |
77 | + context = lookup_oauth_context(context) |
78 | + except ValueError: |
79 | + raise UnexpectedFormData("Unknown context.") |
80 | self.token_context = context |
81 | |
82 | def reviewToken(self, permission): |
83 | @@ -195,6 +188,25 @@ |
84 | self.next_url = ( |
85 | '+token-authorized?oauth_token=%s' % self.token.key) |
86 | |
87 | +def lookup_oauth_context(context): |
88 | + """Transform an OAuth context string into a context object. |
89 | + |
90 | + :param context: A string to turn into a context object. |
91 | + """ |
92 | + if '/' in context: |
93 | + distro, package = context.split('/') |
94 | + distro = getUtility(IDistributionSet).getByName(distro) |
95 | + if distro is None: |
96 | + raise ValueError(distro) |
97 | + context = distro.getSourcePackage(package) |
98 | + if context is None: |
99 | + raise ValueError(package) |
100 | + else: |
101 | + context = getUtility(IPillarNameSet).getByName(context) |
102 | + if context is None: |
103 | + raise ValueError(context) |
104 | + return context |
105 | + |
106 | |
107 | class OAuthTokenAuthorizedView(LaunchpadView): |
108 | """Where users who reviewed tokens may get redirected to. |
109 | |
110 | === modified file 'lib/canonical/launchpad/doc/oauth.txt' |
111 | --- lib/canonical/launchpad/doc/oauth.txt 2010-03-11 01:39:25 +0000 |
112 | +++ lib/canonical/launchpad/doc/oauth.txt 2010-04-05 16:09:33 +0000 |
113 | @@ -430,3 +430,57 @@ |
114 | Traceback (most recent call last): |
115 | ... |
116 | TimestampOrderingError: ... |
117 | + |
118 | + |
119 | +Helper methods |
120 | +============== |
121 | + |
122 | +The oauth_access_token_for() helper function makes it easy to get an |
123 | +access token for any user, consumer key, permission, and context. |
124 | + |
125 | +If the user already has an access token that does what you need, |
126 | +oauth_access_token_for() returns the existing token. |
127 | + |
128 | + >>> from lp.testing import oauth_access_token_for |
129 | + >>> existing_token = salgado.oauth_access_tokens[0] |
130 | + >>> token = oauth_access_token_for( |
131 | + ... existing_token.consumer.key, existing_token.person, |
132 | + ... existing_token.permission, existing_token.context) |
133 | + |
134 | + >>> from zope.proxy import sameProxiedObjects |
135 | + >>> sameProxiedObjects(token, existing_token) |
136 | + True |
137 | + |
138 | +If the user does not already have an access token that matches your |
139 | +requirements, oauth_access_token_for() creates a request token and |
140 | +automatically authorizes it. Here, we create a brand new token for a |
141 | +never-before-seen consumer. |
142 | + |
143 | + >>> new_consumer = 'new consumer key to test oauth_access_token_for' |
144 | + >>> token = oauth_access_token_for( |
145 | + ... new_consumer, salgado, 'WRITE_PRIVATE', firefox) |
146 | + |
147 | + >>> print token.consumer.key |
148 | + new consumer key to test oauth_access_token_for |
149 | + |
150 | + >>> print token.person.name |
151 | + salgado |
152 | + |
153 | + >>> token.permission |
154 | + <DBItem AccessLevel.WRITE_PRIVATE...> |
155 | + |
156 | + >>> print token.context.name |
157 | + firefox |
158 | + |
159 | + >>> print token.date_expires |
160 | + None |
161 | + |
162 | +You can use the token identifying one of Launchpad's OAuth permission |
163 | +levels instead of the constant itself, but if you specify a |
164 | +nonexistent permission you'll get an error. |
165 | + |
166 | + >>> oauth_access_token_for( |
167 | + ... new_consumer, salgado, 'NO_SUCH_PERMISSION', firefox) |
168 | + Traceback (most recent call last): |
169 | + ... |
170 | + KeyError: 'NO_SUCH_PERMISSION' |
171 | |
172 | === added file 'lib/canonical/launchpad/pagetests/webservice/launchpadlib.txt' |
173 | --- lib/canonical/launchpad/pagetests/webservice/launchpadlib.txt 1970-01-01 00:00:00 +0000 |
174 | +++ lib/canonical/launchpad/pagetests/webservice/launchpadlib.txt 2010-04-05 16:09:33 +0000 |
175 | @@ -0,0 +1,50 @@ |
176 | +Using launchpadlib in pagetests |
177 | +=============================== |
178 | + |
179 | +As an alternative to crafting HTTP requests with the 'webservice' |
180 | +object, you can write pagetests using launchpadlib. |
181 | + |
182 | +Two helper functions make it easy to set up Launchpad objects that |
183 | +can access the web service. With launchpadlib_for() you can set up a |
184 | +Launchpad object for a given user with a single call. |
185 | + |
186 | + >>> launchpad = launchpadlib_for( |
187 | + ... 'launchpadlib test', 'salgado', 'WRITE_PUBLIC') |
188 | + >>> print launchpad.me.name |
189 | + salgado |
190 | + |
191 | + # XXX leonardr 2010-03-31 bug=552732 |
192 | + # launchpadlib doesn't work with a credential scoped to a context |
193 | + # like 'firefox', because the service root resource is considered |
194 | + # out of scope. This test should pass, but it doesn't. |
195 | + # |
196 | + # When you fix this, be sure to show that an attempt to access |
197 | + # something that really is out of scope (like launchpad.me.name) |
198 | + # yields a 401 error. |
199 | + # |
200 | + #>>> launchpad = launchpadlib_for( |
201 | + #... 'launchpadlib test', 'no-priv', 'READ_PRIVATE', 'firefox', |
202 | + #... version="devel") |
203 | + #>>> print launchpad.projects['firefox'].name |
204 | + #firefox |
205 | + |
206 | +With launchpadlib_credentials_for() you can get a launchpadlib |
207 | +Credentials object. |
208 | + |
209 | + >>> from lp.testing import launchpadlib_credentials_for |
210 | + >>> credentials = launchpadlib_credentials_for( |
211 | + ... 'launchpadlib test', 'no-priv', 'READ_PUBLIC') |
212 | + >>> credentials |
213 | + <launchpadlib.credentials.Credentials object ...> |
214 | + |
215 | + >>> print credentials.consumer.key |
216 | + launchpadlib test |
217 | + >>> print credentials.access_token |
218 | + oauth_token_secret=...&oauth_token=... |
219 | + |
220 | +This can be used to create your own Launchpad object. |
221 | + |
222 | + >>> from launchpadlib.launchpad import Launchpad |
223 | + >>> launchpad = Launchpad(credentials, 'http://api.launchpad.dev/') |
224 | + >>> print launchpad.me.name |
225 | + no-priv |
226 | |
227 | === modified file 'lib/canonical/launchpad/pagetests/webservice/xx-wadl.txt' |
228 | --- lib/canonical/launchpad/pagetests/webservice/xx-wadl.txt 2010-03-26 13:23:20 +0000 |
229 | +++ lib/canonical/launchpad/pagetests/webservice/xx-wadl.txt 2010-04-05 16:09:33 +0000 |
230 | @@ -3,55 +3,83 @@ |
231 | Because Launchpad's main WADL files are so big, we cache them |
232 | internally: one WADL file for every version of the web service. |
233 | Because the WADL only changes when the Launchpad software changes, |
234 | -these documents are cached to files. Right now, the Launchpad |
235 | -webservice serves a special test file |
236 | -(canonical/launchpad/apidoc/wadl-testrunner-devel.xml) when a client |
237 | -asks for the big WADL definition for the 'devel' version. The |
238 | -'testrunner' part comes from canonical.config.config.instance_name, so |
239 | -a development instance will use the file |
240 | -canonical/launchpad/apidoc/wadl-development-{version}.xml. |
241 | - |
242 | - >>> test_wadl = webservice.get( |
243 | - ... '/', 'application/vd.sun.wadl+xml', api_version='devel').body |
244 | - >>> print test_wadl |
245 | - <?xml version="1.0"?> |
246 | - <wadl:application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
247 | - xmlns="http://research.sun.com/wadl/2006/10" |
248 | - xmlns:wadl="http://research.sun.com/wadl/2006/10" |
249 | - xsi:schemaLocation="http://research.sun.com/wadl/2006/10/wadl.xsd"> |
250 | - <BLANKLINE> |
251 | - <!--This file is for testing purposes only. See |
252 | - canonical/launcpad/pagetests/webservice/xx-wadl.txt --> |
253 | - <BLANKLINE> |
254 | - </wadl:application> |
255 | - <BLANKLINE> |
256 | - |
257 | -Let's look at the real contents, though. To do this, we need to |
258 | -deactivate the cache. Simply clearing it out will just cause it to be |
259 | -filled up again. |
260 | +these documents are cached to files. |
261 | + |
262 | +This test shows how the cache works. We'll start by temporarily |
263 | +clearing the cache. |
264 | |
265 | >>> from canonical.launchpad.systemhomes import WebServiceApplication |
266 | >>> old_cached_wadl = WebServiceApplication.cached_wadl |
267 | - >>> WebServiceApplication.cached_wadl = None |
268 | + >>> WebServiceApplication.cached_wadl = {} |
269 | + |
270 | +If WADL is present in a certain file on disk--the filename depends on |
271 | +the Launchpad configuration and the web service version--it will be |
272 | +loaded from disk and not generated from scratch. But the testrunner |
273 | +does not have any WADL files written to disk. |
274 | + |
275 | + >>> import os |
276 | + >>> wadl_filename = WebServiceApplication.cachedWADLPath( |
277 | + ... 'testrunner', 'devel') |
278 | + >>> os.path.exists(wadl_filename) |
279 | + False |
280 | + |
281 | +Let's write some fake WADL to disk. |
282 | + |
283 | + >>> fd = open(wadl_filename, "w") |
284 | + >>> fd.write("Some fake WADL.") |
285 | + >>> fd.close() |
286 | + |
287 | +When we request the WADL for version "devel", the fake WADL is loaded |
288 | +from disk. |
289 | + |
290 | + >>> print webservice.get( |
291 | + ... '/', 'application/vd.sun.wadl+xml', api_version='devel').body |
292 | + Some fake WADL. |
293 | + |
294 | +The fake WADL is now present in the cache. |
295 | + |
296 | + >>> WebServiceApplication.cached_wadl |
297 | + {u'devel': u'Some fake WADL.'} |
298 | + |
299 | +Change the cached value, and we change the document served. |
300 | + |
301 | + >>> WebServiceApplication.cached_wadl['devel'] = "More fake WADL." |
302 | + |
303 | + >>> print webservice.get( |
304 | + ... '/', 'application/vd.sun.wadl+xml', api_version='devel').body |
305 | + More fake WADL. |
306 | + |
307 | +If there's no value in the cache and no cached file on disk, the WADL |
308 | +is generated from scratch. |
309 | + |
310 | + >>> WebServiceApplication.cached_wadl = {} |
311 | + >>> os.remove(wadl_filename) |
312 | |
313 | >>> wadl = webservice.get( |
314 | ... '/', 'application/vd.sun.wadl+xml', api_version='devel').body |
315 | >>> wadl = wadl.decode('UTF-8') |
316 | |
317 | -The real file is much bigger than the test file. |
318 | - |
319 | - >>> len(wadl) > len(test_wadl) |
320 | +Unlike the test strings we used earlier, this is a valid WADL file. |
321 | + |
322 | + >>> from lazr.restful import WADL_SCHEMA_FILE |
323 | + >>> from canonical.lazr.xml import XMLValidator |
324 | + >>> wadl_schema = XMLValidator(WADL_SCHEMA_FILE) |
325 | + |
326 | + # We need to replace the nbsp entity, because the validator |
327 | + # doesn't support embedded definition. |
328 | + >>> wadl_schema.validate( |
329 | + ... wadl.replace(' ', ' ').encode('UTF-8')) |
330 | True |
331 | |
332 | The WADL we received is keyed to the 'devel' version of the web |
333 | -service. This version will always be present. |
334 | +service. The URL to this version's service root will always be |
335 | +present. |
336 | |
337 | >>> 'http://api.launchpad.dev/devel/' in wadl |
338 | True |
339 | |
340 | If we retrieve the WADL for the '1.0' version of the web service, |
341 | -we'll get a document keyed to the '1.0' version. The '1.0' version |
342 | -will be deprecated along with the Lucid release of Ubuntu. |
343 | +we'll get a document keyed to the '1.0' version. |
344 | |
345 | >>> wadl_10 = webservice.get( |
346 | ... '/', 'application/vd.sun.wadl+xml', api_version='1.0').body |
347 | @@ -60,31 +88,12 @@ |
348 | >>> 'http://api.launchpad.dev/1.0/' in wadl_10 |
349 | True |
350 | |
351 | -If we retrieve the WADL for the 'beta' version of the web service, |
352 | -we'll get a document keyed to the 'beta' version. The '1.0' version |
353 | -will be deprecated along with the Karmic release of Ubuntu. |
354 | - |
355 | - >>> wadl_beta = webservice.get( |
356 | - ... '/', 'application/vd.sun.wadl+xml', api_version='beta').body |
357 | - >>> wadl_beta = wadl_beta.decode('UTF-8') |
358 | - |
359 | - >>> 'http://api.launchpad.dev/beta/' in wadl_beta |
360 | - True |
361 | - |
362 | -We don't need the cache anymore, so we'll reinstate the testing |
363 | -version. This way, other tests will have a clean slate. |
364 | +All of these documents were cached as they were generated: |
365 | + |
366 | + >>> sorted(WebServiceApplication.cached_wadl.keys()) |
367 | + [u'1.0', u'devel'] |
368 | + |
369 | +Finally, restore the cache so that other tests will have a clean |
370 | +slate. |
371 | |
372 | >>> WebServiceApplication.cached_wadl = old_cached_wadl |
373 | - |
374 | -Like all lazr.restful applications, Launchpad's web service generates |
375 | -valid WADL. |
376 | - |
377 | - >>> from lazr.restful import WADL_SCHEMA_FILE |
378 | - >>> from canonical.lazr.xml import XMLValidator |
379 | - >>> wadl_schema = XMLValidator(WADL_SCHEMA_FILE) |
380 | - |
381 | - # We need to replace the nbsp entity, because the validator |
382 | - # doesn't support embedded definition. |
383 | - >>> wadl_schema.validate( |
384 | - ... wadl.replace(' ', ' ').encode('UTF-8')) |
385 | - True |
386 | |
387 | === modified file 'lib/canonical/launchpad/systemhomes.py' |
388 | --- lib/canonical/launchpad/systemhomes.py 2010-03-26 21:03:30 +0000 |
389 | +++ lib/canonical/launchpad/systemhomes.py 2010-04-05 16:09:33 +0000 |
390 | @@ -354,6 +354,13 @@ |
391 | |
392 | cached_wadl = {} |
393 | |
394 | + @classmethod |
395 | + def cachedWADLPath(cls, instance_name, version): |
396 | + """Helper method to calculate the path to a cached WADL file.""" |
397 | + return os.path.join( |
398 | + os.path.dirname(os.path.normpath(__file__)), |
399 | + 'apidoc', 'wadl-%s-%s.xml' % (instance_name, version)) |
400 | + |
401 | def toWADL(self): |
402 | """See `IWebServiceApplication`. |
403 | |
404 | @@ -370,10 +377,8 @@ |
405 | return super(WebServiceApplication, self).toWADL() |
406 | if version not in self.__class__.cached_wadl: |
407 | # It's not cached. Look for it on disk. |
408 | - _wadl_filename = os.path.join( |
409 | - os.path.dirname(os.path.normpath(__file__)), |
410 | - 'apidoc', 'wadl-%s-%s.xml' % (config.instance_name, version)) |
411 | - |
412 | + _wadl_filename = self.cachedWADLPath( |
413 | + config.instance_name, version) |
414 | _wadl_fd = None |
415 | try: |
416 | _wadl_fd = codecs.open(_wadl_filename, encoding='UTF-8') |
417 | |
418 | === modified file 'lib/canonical/launchpad/testing/pages.py' |
419 | --- lib/canonical/launchpad/testing/pages.py 2010-03-26 13:23:20 +0000 |
420 | +++ lib/canonical/launchpad/testing/pages.py 2010-04-05 16:09:33 +0000 |
421 | @@ -29,6 +29,8 @@ |
422 | from zope.testing import doctest |
423 | from zope.security.proxy import removeSecurityProxy |
424 | |
425 | +from launchpadlib.launchpad import Launchpad |
426 | + |
427 | from canonical.launchpad.interfaces import ( |
428 | IOAuthConsumerSet, OAUTH_REALM, ILaunchpadCelebrities, |
429 | TeamMembershipStatus) |
430 | @@ -39,7 +41,8 @@ |
431 | from canonical.launchpad.webapp.url import urlsplit |
432 | from canonical.testing import PageTestLayer |
433 | from lazr.restful.testing.webservice import WebServiceCaller |
434 | -from lp.testing import ANONYMOUS, login, login_person, logout |
435 | +from lp.testing import ( |
436 | + ANONYMOUS, launchpadlib_for, login, login_person, logout) |
437 | from lp.testing.factory import LaunchpadObjectFactory |
438 | from lp.registry.interfaces.person import NameAlreadyTaken |
439 | |
440 | @@ -752,6 +755,7 @@ |
441 | test.globs['print_table'] = print_table |
442 | test.globs['extract_link_from_tag'] = extract_link_from_tag |
443 | test.globs['extract_text'] = extract_text |
444 | + test.globs['launchpadlib_for'] = launchpadlib_for |
445 | test.globs['login'] = login |
446 | test.globs['login_person'] = login_person |
447 | test.globs['logout'] = logout |
448 | |
449 | === modified file 'lib/canonical/testing/layers.py' |
450 | --- lib/canonical/testing/layers.py 2010-03-26 13:23:20 +0000 |
451 | +++ lib/canonical/testing/layers.py 2010-04-05 16:09:33 +0000 |
452 | @@ -73,6 +73,7 @@ |
453 | from storm.zope.interfaces import IZStorm |
454 | import transaction |
455 | import wsgi_intercept |
456 | +from wsgi_intercept import httplib2_intercept |
457 | |
458 | from lazr.restful.utils import safe_hasattr |
459 | |
460 | @@ -938,12 +939,18 @@ |
461 | register_launchpad_request_publication_factories() |
462 | wsgi_intercept.add_wsgi_intercept( |
463 | 'localhost', 80, lambda: wsgi_application) |
464 | + wsgi_intercept.add_wsgi_intercept( |
465 | + 'api.launchpad.dev', 80, lambda: wsgi_application) |
466 | + httplib2_intercept.install() |
467 | + |
468 | |
469 | @classmethod |
470 | @profiled |
471 | def tearDown(cls): |
472 | FunctionalLayer.isSetUp = False |
473 | wsgi_intercept.remove_wsgi_intercept('localhost', 80) |
474 | + wsgi_intercept.remove_wsgi_intercept('api.launchpad.dev', 80) |
475 | + httplib2_intercept.uninstall() |
476 | # Signal Layer cannot be torn down fully |
477 | raise NotImplementedError |
478 | |
479 | |
480 | === modified file 'lib/lp/testing/__init__.py' |
481 | --- lib/lp/testing/__init__.py 2010-03-30 21:49:22 +0000 |
482 | +++ lib/lp/testing/__init__.py 2010-04-05 16:09:33 +0000 |
483 | @@ -82,6 +82,8 @@ |
484 | # canonical.launchpad.ftests expects test_tales to be imported from here. |
485 | # XXX: JonathanLange 2010-01-01: Why?! |
486 | from lp.testing._tales import test_tales |
487 | +from lp.testing._webservice import ( |
488 | + launchpadlib_credentials_for, launchpadlib_for, oauth_access_token_for) |
489 | |
490 | # zope.exception demands more of frame objects than twisted.python.failure |
491 | # provides in its fake frames. This is enough to make it work with them |
492 | |
493 | === added file 'lib/lp/testing/_webservice.py' |
494 | --- lib/lp/testing/_webservice.py 1970-01-01 00:00:00 +0000 |
495 | +++ lib/lp/testing/_webservice.py 2010-04-05 16:09:33 +0000 |
496 | @@ -0,0 +1,119 @@ |
497 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
498 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
499 | + |
500 | +# We like global statements! |
501 | +# pylint: disable-msg=W0602,W0603 |
502 | +__metaclass__ = type |
503 | + |
504 | +__all__ = [ |
505 | + 'launchpadlib_credentials_for', |
506 | + 'launchpadlib_for', |
507 | + 'oauth_access_token_for' |
508 | + ] |
509 | + |
510 | +from zope.component import getUtility |
511 | +from launchpadlib.credentials import AccessToken, Credentials |
512 | +from launchpadlib.launchpad import Launchpad |
513 | + |
514 | +from canonical.launchpad.webapp.interfaces import OAuthPermission |
515 | +from canonical.launchpad.interfaces import ( |
516 | + IOAuthConsumerSet, IPersonSet) |
517 | + |
518 | +from lp.testing._login import ANONYMOUS, login, logout |
519 | + |
520 | +def oauth_access_token_for(consumer_name, person, permission, context=None): |
521 | + """Find or create an OAuth access token for the given person. |
522 | + :param consumer_name: An OAuth consumer name. |
523 | + :param person: A person (or the name of a person) for whom to create |
524 | + or find credentials. |
525 | + :param permission: An OAuthPermission (or its token) designating |
526 | + the level of permission the credentials should have. |
527 | + :param context: The OAuth context for the credentials (or a string |
528 | + designating same). |
529 | + |
530 | + :return: An OAuthAccessToken object. |
531 | + """ |
532 | + if isinstance(person, basestring): |
533 | + # Look up a person by name. |
534 | + person = getUtility(IPersonSet).getByName(person) |
535 | + if isinstance(context, basestring): |
536 | + # Turn an OAuth context string into the corresponding object. |
537 | + # Avoid an import loop by importing from launchpad.browser here. |
538 | + from canonical.launchpad.browser.oauth import lookup_oauth_context |
539 | + context = lookup_oauth_context(context) |
540 | + if isinstance(permission, basestring): |
541 | + # Look up a permission by its token string. |
542 | + permission = OAuthPermission.items[permission] |
543 | + |
544 | + # Find or create the consumer object. |
545 | + consumer_set = getUtility(IOAuthConsumerSet) |
546 | + consumer = consumer_set.getByKey(consumer_name) |
547 | + if consumer is None: |
548 | + consumer = consumer_set.new(consumer_name) |
549 | + else: |
550 | + # We didn't have to create the consumer. Maybe this user |
551 | + # already has an access token for this |
552 | + # consumer+person+permission? |
553 | + existing_token = [token for token in person.oauth_access_tokens |
554 | + if (token.consumer == consumer |
555 | + and token.permission == permission |
556 | + and token.context == context)] |
557 | + if len(existing_token) >= 1: |
558 | + return existing_token[0] |
559 | + |
560 | + # There is no existing access token for this |
561 | + # consumer+person+permission+context. Create one and review it. |
562 | + request_token = consumer.newRequestToken() |
563 | + request_token.review(person, permission, context) |
564 | + access_token = request_token.createAccessToken() |
565 | + return access_token |
566 | + |
567 | + |
568 | +def launchpadlib_credentials_for( |
569 | + consumer_name, person, permission=OAuthPermission.WRITE_PRIVATE, |
570 | + context=None): |
571 | + """Create launchpadlib credentials for the given person. |
572 | + |
573 | + :param consumer_name: An OAuth consumer name. |
574 | + :param person: A person (or the name of a person) for whom to create |
575 | + or find credentials. |
576 | + :param permission: An OAuthPermission (or its token) designating |
577 | + the level of permission the credentials should have. |
578 | + :param context: The OAuth context for the credentials. |
579 | + :return: A launchpadlib Credentials object. |
580 | + """ |
581 | + # Start an interaction so that oauth_access_token_for will |
582 | + # succeed. oauth_access_token_for may be called in any layer, but |
583 | + # launchpadlib_credentials_for is only called in the |
584 | + # PageTestLayer, when a Launchpad instance is running for |
585 | + # launchpadlib to use. |
586 | + login(ANONYMOUS) |
587 | + access_token = oauth_access_token_for( |
588 | + consumer_name, person, permission, context) |
589 | + logout() |
590 | + launchpadlib_token = AccessToken( |
591 | + access_token.key, access_token.secret) |
592 | + return Credentials(consumer_name=consumer_name, |
593 | + access_token=launchpadlib_token) |
594 | + |
595 | + |
596 | +def launchpadlib_for( |
597 | + consumer_name, person, permission=OAuthPermission.WRITE_PRIVATE, |
598 | + context=None, version=None, service_root="http://api.launchpad.dev/"): |
599 | + """Create a Launchpad object for the given person. |
600 | + |
601 | + :param consumer_name: An OAuth consumer name. |
602 | + :param person: A person (or the name of a person) for whom to create |
603 | + or find credentials. |
604 | + :param permission: An OAuthPermission (or its token) designating |
605 | + the level of permission the credentials should have. |
606 | + :param context: The OAuth context for the credentials. |
607 | + :param version: The version of the web service to access. |
608 | + :param service_root: The root URL of the web service to access. |
609 | + |
610 | + :return: A launchpadlib Launchpad object. |
611 | + """ |
612 | + credentials = launchpadlib_credentials_for( |
613 | + consumer_name, person, permission, context) |
614 | + version = version or Launchpad.DEFAULT_VERSION |
615 | + return Launchpad(credentials, service_root, version=version) |
wgrant informs me that this branch _did_ cause test failures everywhere, but the failures were so bad the test suite didn't register them as failures.