Merge lp:~leonardr/launchpadlib/trusted-workflow-tests into lp:launchpadlib
- trusted-workflow-tests
- Merge into trunk
Proposed by
Leonard Richardson
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~leonardr/launchpadlib/trusted-workflow-tests |
Merge into: | lp:launchpadlib |
Diff against target: |
586 lines 5 files modified
src/launchpadlib/apps.py (+131/-1) src/launchpadlib/bin/launchpad-credentials-console (+54/-0) src/launchpadlib/credentials.py (+37/-17) src/launchpadlib/docs/command-line.txt (+173/-3) src/launchpadlib/testing/helpers.py (+28/-3) |
To merge this branch: | bzr merge lp:~leonardr/launchpadlib/trusted-workflow-tests |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Abel Deuring (community) | code | Approve | |
Review via email: mp+14167@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote : | # |
Revision history for this message
Abel Deuring (adeuring) : | # |
review:
Approve
(code)
- 72. By Leonard Richardson
-
Fixed up tests and return codes.
- 73. By Leonard Richardson
-
Added docstrings.
- 74. By Leonard Richardson
-
Cleaned up the exit function.
- 75. By Leonard Richardson
-
Added another docstring.
- 76. By Leonard Richardson
-
Added the command-line script, made a lot of spacing tweaks.
- 77. By Leonard Richardson
-
Merged with trunk.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/launchpadlib/apps.py' |
2 | --- src/launchpadlib/apps.py 2009-10-28 17:27:53 +0000 |
3 | +++ src/launchpadlib/apps.py 2009-10-30 19:21:14 +0000 |
4 | @@ -20,9 +20,18 @@ |
5 | themselves are kept in bin/. |
6 | """ |
7 | |
8 | +__all__ = [ |
9 | + 'RequestTokenApp', |
10 | + 'TrustedTokenAuthorizationConsoleApp', |
11 | + ] |
12 | + |
13 | +import getpass |
14 | +import sys |
15 | + |
16 | import simplejson |
17 | |
18 | -from launchpadlib.credentials import Credentials |
19 | +from launchpadlib.credentials import ( |
20 | + Credentials, RequestTokenAuthorizationEngine, TokenAuthorizationException) |
21 | from launchpadlib.uris import lookup_web_root |
22 | |
23 | |
24 | @@ -43,3 +52,124 @@ |
25 | return simplejson.dumps(token) |
26 | |
27 | |
28 | +class TrustedTokenAuthorizationConsoleApp(RequestTokenAuthorizationEngine): |
29 | + """An application that authorizes request tokens.""" |
30 | + |
31 | + def __init__(self, web_root, consumer_name, request_token, |
32 | + access_levels='', input_method=raw_input): |
33 | + """Constructor. |
34 | + |
35 | + :param access_levels: A string of comma-separated access level |
36 | + values. To get an up-to-date list of access levels, pass |
37 | + token_format=Credentials.DICT_TOKEN_FORMAT into |
38 | + Credentials.get_request_token, load the dict as JSON, and look |
39 | + in 'access_levels'. |
40 | + """ |
41 | + access_levels = [level.strip() for level in access_levels.split(',')] |
42 | + super(TrustedTokenAuthorizationConsoleApp, self).__init__( |
43 | + web_root, consumer_name, request_token, access_levels) |
44 | + |
45 | + self.input_method = input_method |
46 | + |
47 | + def run(self): |
48 | + """Try to authorize a request token from user input.""" |
49 | + self.error_code = -1 # Start off assuming failure. |
50 | + start = "Launchpad credential client (console)" |
51 | + self.output(start) |
52 | + self.output("-" * len(start)) |
53 | + |
54 | + try: |
55 | + self() |
56 | + except TokenAuthorizationException, e: |
57 | + print str(e) |
58 | + self.error_code = -1 |
59 | + return self.pressEnterToExit() |
60 | + return self.pressEnterToExit() |
61 | + |
62 | + def exit_with(self, code): |
63 | + """Exit the app with the specified error code.""" |
64 | + sys.exit(code) |
65 | + |
66 | + def getSingleCharInput(self, prompt, valid): |
67 | + """Retrieve a single-character line from the input stream.""" |
68 | + valid = valid.upper() |
69 | + input = None |
70 | + while input is None: |
71 | + input = self.input_method(prompt).upper() |
72 | + if len(input) != 1 or input not in valid: |
73 | + input = None |
74 | + return input |
75 | + |
76 | + def pressEnterToExit(self): |
77 | + """Make the user hit enter, and then exit with an error code.""" |
78 | + prompt = '\nPress enter to go back to "%s". ' % self.consumer_name |
79 | + self.input_method(prompt) |
80 | + self.exit_with(self.error_code) |
81 | + return self.error_code |
82 | + |
83 | + def input_username(self, cached_username, suggested_message): |
84 | + """Collect the Launchpad username from the end-user. |
85 | + |
86 | + :param cached_username: A username from a previous entry attempt, |
87 | + to be presented as the default. |
88 | + """ |
89 | + if cached_username is not None: |
90 | + extra = " [%s] " % cached_username |
91 | + else: |
92 | + extra = "\n(No Launchpad account? Just hit enter.) " |
93 | + username = self.input_method(suggested_message + extra) |
94 | + if username == '': |
95 | + return cached_username |
96 | + return username |
97 | + |
98 | + def input_password(self, suggested_message): |
99 | + """Collect the Launchpad password from the end-user.""" |
100 | + if self.input_method is raw_input: |
101 | + password = getpass.getpass(suggested_message + " ") |
102 | + else: |
103 | + password = self.input_method(suggested_message) |
104 | + return password |
105 | + |
106 | + def input_access_level(self, available_levels, suggested_message, |
107 | + only_one_option=None): |
108 | + """Collect the desired level of access from the end-user.""" |
109 | + if only_one_option is not None: |
110 | + self.output(suggested_message) |
111 | + prompt = self.message( |
112 | + 'Do you want to give "%(app)s" this level of access? [YN] ') |
113 | + allow = self.getSingleCharInput(prompt, "YN") |
114 | + if allow == "Y": |
115 | + return only_one_option['value'] |
116 | + else: |
117 | + return self.UNAUTHORIZED_ACCESS_LEVEL |
118 | + else: |
119 | + levels_except_unauthorized = [ |
120 | + level for level in available_levels |
121 | + if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL] |
122 | + options = [] |
123 | + for i in range(0, len(levels_except_unauthorized)): |
124 | + options.append( |
125 | + "%d: %s" % (i+1, levels_except_unauthorized[i]['title'])) |
126 | + self.output(suggested_message) |
127 | + for option in options: |
128 | + self.output(option) |
129 | + allowed = ("".join(map(str, range(1, i+2)))) + "Q" |
130 | + prompt = self.message( |
131 | + 'What should "%(app)s" be allowed to do using your ' |
132 | + 'Launchpad account? [1-%(max)d or Q] ', |
133 | + extra_variables = {'max' : i+1}) |
134 | + allow = self.getSingleCharInput(prompt, allowed) |
135 | + if allow == "Q": |
136 | + return self.UNAUTHORIZED_ACCESS_LEVEL |
137 | + else: |
138 | + return levels_except_unauthorized[int(allow)-1]['value'] |
139 | + |
140 | + def user_refused_to_authorize(self, suggested_message): |
141 | + """The user refused to authorize a request token.""" |
142 | + self.output(suggested_message) |
143 | + self.error_code = -2 |
144 | + |
145 | + def user_authorized(self, access_level, suggested_message): |
146 | + """The user authorized a request token with some access level.""" |
147 | + self.output(suggested_message) |
148 | + self.error_code = 0 |
149 | |
150 | === added file 'src/launchpadlib/bin/launchpad-credentials-console' |
151 | --- src/launchpadlib/bin/launchpad-credentials-console 1970-01-01 00:00:00 +0000 |
152 | +++ src/launchpadlib/bin/launchpad-credentials-console 2009-10-30 19:21:14 +0000 |
153 | @@ -0,0 +1,54 @@ |
154 | +#!/usr/bin/python |
155 | + |
156 | +# Copyright 2009 Canonical Ltd. |
157 | + |
158 | +# This file is part of launchpadlib. |
159 | +# |
160 | +# launchpadlib is free software: you can redistribute it and/or modify it |
161 | +# under the terms of the GNU Lesser General Public License as published by the |
162 | +# Free Software Foundation, version 3 of the License. |
163 | +# |
164 | +# launchpadlib is distributed in the hope that it will be useful, but WITHOUT |
165 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
166 | +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License |
167 | +# for more details. |
168 | +# |
169 | +# You should have received a copy of the GNU Lesser General Public License |
170 | +# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>. |
171 | + |
172 | +"""Take the user's Launchpad credentials and authorize a request token. |
173 | + |
174 | +This script will guide the user through the process of authorizing a |
175 | +request token with their username and password. It's not as secure as |
176 | +opening the user's web browser, but if you are writing a console |
177 | +application and want to keep the authorization process console-based, |
178 | +it's better to use this script than write the authorization code yourself. |
179 | +""" |
180 | + |
181 | +__metaclass__ = type |
182 | + |
183 | +from optparse import OptionParser |
184 | +from launchpadlib.apps import ( |
185 | + RequestTokenApp, TrustedTokenAuthorizationConsoleApp) |
186 | + |
187 | +parser = OptionParser() |
188 | +parser.usage = "%prog CONSUMER_NAME REQUEST_TOKEN [-r URL] [-a LEVEL,LEVEL,...]" |
189 | +parser.add_option("-s", "--site", dest="web_root", |
190 | + help=("The name of the Launchpad site on which the request " |
191 | + "token can be found (default: %default). This can " |
192 | + "also be the URL to the root of the site."), |
193 | + metavar="URL", default="staging") |
194 | +parser.add_option("-a", "--access-level", dest="access_levels", |
195 | + help="Any restrictions on your clients' access levels", |
196 | + metavar="[LEVEL,LEVEL,...]", default="") |
197 | + |
198 | +if __name__ == '__main__': |
199 | + (options, args) = parser.parse_args() |
200 | + if len(args) < 2: |
201 | + parser.error("You must provide both a consumer name and the ID of " |
202 | + "an OAuth request token on Launchpad.") |
203 | + consumer_name, token_id = args[:2] |
204 | + TrustedTokenAuthorizationConsoleApp( |
205 | + options.web_root, consumer_name, token_id, |
206 | + options.access_levels).run() |
207 | + |
208 | |
209 | === modified file 'src/launchpadlib/credentials.py' |
210 | --- src/launchpadlib/credentials.py 2009-10-30 14:48:21 +0000 |
211 | +++ src/launchpadlib/credentials.py 2009-10-30 19:21:14 +0000 |
212 | @@ -27,8 +27,10 @@ |
213 | import base64 |
214 | import cgi |
215 | import httplib2 |
216 | +import textwrap |
217 | from urllib import urlencode, quote |
218 | from urlparse import urljoin |
219 | +import webbrowser |
220 | |
221 | import simplejson |
222 | |
223 | @@ -265,7 +267,7 @@ |
224 | |
225 | INPUT_USERNAME = "What email address do you use on Launchpad?" |
226 | |
227 | - INPUT_PASSWORD = "What's your Launchpad password?" |
228 | + INPUT_PASSWORD = "What's your Launchpad password? " |
229 | |
230 | NONEXISTENT_REQUEST_TOKEN = """Launchpad couldn't find an outstanding request for integration between "%(app)s" and your Launchpad account. Either someone (hopefully you) already set up the integration, or else "%(app)s" is simply wrong and didn't actually set this up with Launchpad. If you still can't use "%(app)s" with Launchpad, try this process again from the beginning.""" |
231 | |
232 | @@ -273,9 +275,9 @@ |
233 | |
234 | SERVER_ERROR = """There seems to be something wrong on the Launchpad server side, and I can't continue. Hopefully this is a temporary problem, but if it persists, it's probably because of a bug in Lauchpad or (less likely) a bug in "%(app)s".""" |
235 | |
236 | - STARTUP_MESSAGE = """An application identified as "%(app)s" wants to access Launchpad on your behalf. I'm the Launchpad credential client and I'm here to ask for your Launchpad username and password. |
237 | + STARTUP_MESSAGE = """An application identified as "%(app)s" wants to access Launchpad on your behalf. I'm the Launchpad credential client and I'm here to ask for your Launchpad username and password.""" |
238 | |
239 | -I'll use your Launchpad password to give "%(app)s" limited access to your Launchpad account. I will not show your password to "%(app)s" itself.""" |
240 | + STARTUP_MESSAGE_2 = """I'll use your Launchpad password to give "%(app)s" limited access to your Launchpad account. I will not show your password to "%(app)s" itself.""" |
241 | |
242 | SUCCESS = """You're all done! You should now be able to use Launchpad integration features of "%(app)s." """ |
243 | |
244 | @@ -289,16 +291,20 @@ |
245 | |
246 | def __init__(self, web_root, consumer_name, request_token, |
247 | allow_access_levels=[], max_failed_attempts=3): |
248 | - self.web_root = web_root |
249 | + self.web_root = uris.lookup_web_root(web_root) |
250 | self.consumer_name = consumer_name |
251 | self.request_token = request_token |
252 | self.browser = SimulatedLaunchpadBrowser(self.web_root) |
253 | self.max_failed_attempts = max_failed_attempts |
254 | self.allow_access_levels = allow_access_levels |
255 | + self.text_wrapper = textwrap.TextWrapper( |
256 | + replace_whitespace=False, width=78) |
257 | |
258 | def __call__(self): |
259 | |
260 | - self.startup(self.message(self.STARTUP_MESSAGE)) |
261 | + self.startup( |
262 | + [self.message(self.STARTUP_MESSAGE), |
263 | + self.message(self.STARTUP_MESSAGE_2)]) |
264 | |
265 | # Have the end-user enter their Launchpad username and password. |
266 | # Make sure the credentials are valid, and get information |
267 | @@ -315,14 +321,17 @@ |
268 | # There's only one choice: allow access at a certain level |
269 | # or don't allow access at all. |
270 | message = self.CHOOSE_ACCESS_LEVEL_ONE |
271 | - level = [level['title'] for level in self.reconciled_access_levels |
272 | - if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL] |
273 | - extra = {'level' : level[0]} |
274 | + level = [level for level in self.reconciled_access_levels |
275 | + if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL][0] |
276 | + extra = {'level' : level['title']} |
277 | + only_one_option = level |
278 | else: |
279 | message = self.CHOOSE_ACCESS_LEVEL |
280 | extra = None |
281 | + only_one_option = None |
282 | access_level = self.input_access_level( |
283 | - self.reconciled_access_levels, self.message(message, extra)) |
284 | + self.reconciled_access_levels, self.message(message, extra), |
285 | + only_one_option) |
286 | |
287 | # Notify the program of the user's choice. |
288 | if access_level == self.UNAUTHORIZED_ACCESS_LEVEL: |
289 | @@ -420,8 +429,8 @@ |
290 | |
291 | # You should define these methods in your subclass. |
292 | |
293 | - def startup(self, suggested_message): |
294 | - """Hook method called on startup.""" |
295 | + def output(self, message): |
296 | + print self.text_wrapper.fill(message) |
297 | |
298 | def input_username(self, cached_username, suggested_message): |
299 | """Collect the Launchpad username from the end-user. |
300 | @@ -435,22 +444,31 @@ |
301 | """Collect the Launchpad password from the end-user.""" |
302 | raise NotImplementedError() |
303 | |
304 | - def input_access_level(self, available_levels, suggested_message): |
305 | + def input_access_level(self, available_levels, suggested_message, |
306 | + only_one_option=None): |
307 | """Collect the desired level of access from the end-user.""" |
308 | raise NotImplementedError() |
309 | |
310 | + def startup(self, suggested_messages): |
311 | + """Hook method called on startup.""" |
312 | + for message in suggested_messages: |
313 | + self.output(message) |
314 | + self.output("\n") |
315 | |
316 | def authentication_failure(self, suggested_message): |
317 | """The user entered invalid credentials.""" |
318 | - print suggested_message |
319 | + self.output(suggested_message) |
320 | + self.output("\n") |
321 | |
322 | def user_refused_to_authorize(self, suggested_message): |
323 | """The user refused to authorize a request token.""" |
324 | - print suggested_message |
325 | + self.output(suggested_message) |
326 | + self.output("\n") |
327 | |
328 | def user_authorized(self, access_level, suggested_message): |
329 | """The user authorized a request token with some access level.""" |
330 | - print suggested_message |
331 | + self.output(suggested_message) |
332 | + self.output("\n") |
333 | |
334 | def server_consumer_differs_from_client_consumer( |
335 | self, client_name, real_name, suggested_message): |
336 | @@ -461,11 +479,13 @@ |
337 | end-user that its name is "bar". Something is fishy and at the very |
338 | least the end-user should be warned about this. |
339 | """ |
340 | - print suggested_message |
341 | + self.output("\n") |
342 | + self.output(suggested_message) |
343 | + self.output("\n") |
344 | |
345 | def success(self, suggested_message): |
346 | """The token was successfully authorized.""" |
347 | - print suggested_message |
348 | + self.output(suggested_message) |
349 | |
350 | |
351 | class TokenAuthorizationException(Exception): |
352 | |
353 | === modified file 'src/launchpadlib/docs/command-line.txt' |
354 | --- src/launchpadlib/docs/command-line.txt 2009-10-27 16:26:06 +0000 |
355 | +++ src/launchpadlib/docs/command-line.txt 2009-10-30 19:21:14 +0000 |
356 | @@ -20,9 +20,11 @@ |
357 | |
358 | >>> import simplejson |
359 | >>> from launchpadlib.apps import RequestTokenApp |
360 | - >>> app = RequestTokenApp( |
361 | - ... "http://launchpad.dev:8085/", "consumer", "context") |
362 | - >>> json = simplejson.loads(app.run()) |
363 | + |
364 | + >>> web_root = "http://launchpad.dev:8085/" |
365 | + >>> consumer_name = "consumer" |
366 | + >>> token_app = RequestTokenApp(web_root, consumer_name, "context") |
367 | + >>> json = simplejson.loads(token_app.run()) |
368 | |
369 | >>> sorted(json.keys()) |
370 | ['access_levels', 'lp.context', 'oauth_token', |
371 | @@ -33,3 +35,171 @@ |
372 | |
373 | >>> print json['oauth_token_consumer'] |
374 | consumer |
375 | + |
376 | +TrustedTokenAuthorizationConsoleApp |
377 | +=================================== |
378 | + |
379 | +This class is called by the command-line script |
380 | +launchpad-credentials-console. It asks for the user's Launchpad |
381 | +username and password, and authorizes a request token on their |
382 | +behalf. |
383 | + |
384 | + >>> from launchpadlib.apps import TrustedTokenAuthorizationConsoleApp |
385 | + >>> from launchpadlib.testing.helpers import UserInput |
386 | + |
387 | +This class does not create the request token, or exchange it for the |
388 | +access token--that's the job of the program that calls |
389 | +launchpad-credentials-console. So we'll use the request token created |
390 | +earlier by RequestTokenApp. |
391 | + |
392 | + >>> request_token = json['oauth_token'] |
393 | + |
394 | +Since this is a test, we don't want the application to call sys.exit() |
395 | +or try to open up pages in a web browser. This subclass of |
396 | +TrustedTokenAuthorizationConsoleApp will print messages instead of |
397 | +performing such un-doctest-like actions. |
398 | + |
399 | + >>> class ConsoleApp(TrustedTokenAuthorizationConsoleApp): |
400 | + ... def open_login_page_in_user_browser(self, url): |
401 | + ... """Print a status message.""" |
402 | + ... self.output("[If this were a real application, the " |
403 | + ... "end-user's web browser would be opened " |
404 | + ... "to %s]" % url) |
405 | + ... |
406 | + ... def exit_with(self, code): |
407 | + ... print "Application exited with code %d" % code |
408 | + |
409 | +We'll use a UserInput object to simulate a user typing things in at |
410 | +the prompt and hitting enter. This UserInput runs the program |
411 | +correctly, entering the Launchpad username and password, choosing an |
412 | +access level, and hitting Enter to exit. |
413 | + |
414 | + >>> username = "salgado@ubuntu.com" |
415 | + >>> password = "zeca" |
416 | + >>> fake_input = UserInput([username, password, "1", ""]) |
417 | + |
418 | +Here's a successful run of the application. When the request token is |
419 | +authorized, the script's response code is 0. |
420 | + |
421 | + >>> app = ConsoleApp( |
422 | + ... web_root, consumer_name, request_token, |
423 | + ... 'READ_PRIVATE, READ_PUBLIC', input_method=fake_input) |
424 | + >>> ignore = app.run() |
425 | + Launchpad credential client (console) |
426 | + ------------------------------------- |
427 | + An application identified as "consumer" wants to access Launchpad... |
428 | + What email address do you use on Launchpad? |
429 | + (No Launchpad account? Just hit enter.) [User input: salgado@ubuntu.com] |
430 | + What's your Launchpad password? [User input: zeca] |
431 | + Now it's time for you to decide how much power to give "consumer"... |
432 | + 1: Read Non-Private Data |
433 | + 2: Read Anything |
434 | + What should "consumer" be allowed to do...? [1-2 or Q] [User input: 1] |
435 | + Okay, I'm telling Launchpad to grant "consumer" access to your account. |
436 | + You're all done!... |
437 | + Press enter to go back to "consumer". [User input: ] |
438 | + Application exited with code 0 |
439 | + |
440 | +Now that the request token has been authorized, we'll need to create |
441 | +another one to continue the test. |
442 | + |
443 | + >>> json = simplejson.loads(token_app.run()) |
444 | + >>> request_token = json['oauth_token'] |
445 | + >>> app.request_token = request_token |
446 | + |
447 | +Invalid input is ignored. The user may enter 'Q' instead of a number |
448 | +to refuse to authorize the request token. When the user denies access, |
449 | +the exit code is -2. |
450 | + |
451 | + >>> fake_input = UserInput([username, password, "A", "99", "Q", ""]) |
452 | + >>> app.input_method = fake_input |
453 | + >>> ignore = app.run() |
454 | + Launchpad credential client (console) |
455 | + ------------------------------------- |
456 | + An application identified as "consumer"... |
457 | + What should "consumer" be allowed to do...? [1-2 or Q] [User input: A] |
458 | + What should "consumer" be allowed to do...? [1-2 or Q] [User input: 99] |
459 | + What should "consumer" be allowed to do...? [1-2 or Q] [User input: Q] |
460 | + Okay, I'm going to cancel the request... |
461 | + You're all done! "consumer" still doesn't have access... |
462 | + <BLANKLINE> |
463 | + Press enter to go back to "consumer". [User input: ] |
464 | + Application exited with code -2 |
465 | + |
466 | +When the third-party application will allow only one level of access, |
467 | +the end-user is presented with a yes-or-no choice instead of a list to |
468 | +choose from. Again, invalid input is ignored. |
469 | + |
470 | + >>> json = simplejson.loads(token_app.run()) |
471 | + >>> request_token = json['oauth_token'] |
472 | + >>> fake_input = UserInput([username, password, "1", "Q", "Y", ""]) |
473 | + |
474 | + >>> app = ConsoleApp( |
475 | + ... web_root, consumer_name, request_token, |
476 | + ... 'READ_PRIVATE', input_method=fake_input) |
477 | + |
478 | + >>> ignore = app.run() |
479 | + Launchpad credential client (console) |
480 | + ------------------------------------- |
481 | + An application identified as "consumer"... |
482 | + Do you want to give "consumer" this level of access? [YN] [User input: 1] |
483 | + Do you want to give "consumer" this level of access? [YN] [User input: Q] |
484 | + Do you want to give "consumer" this level of access? [YN] [User input: Y] |
485 | + ... |
486 | + Application exited with code 0 |
487 | + |
488 | + |
489 | +Error handling |
490 | +-------------- |
491 | + |
492 | +When the end-user refuses to authorize the request token, the app |
493 | +exits with a return code of -2, as seen above. When any other error |
494 | +gets in the way of the authorization of the request token, the app's |
495 | +return code is -1. |
496 | + |
497 | +If the user hits enter when asked for their email address, indicating |
498 | +that they don't have a Launchpad account, the app opens their browser |
499 | +to the Launchpad login page. |
500 | + |
501 | + >>> json = simplejson.loads(token_app.run()) |
502 | + >>> app.request_token = json['oauth_token'] |
503 | + |
504 | + >>> input_nothing = UserInput(["", ""]) |
505 | + >>> app.input_method = input_nothing |
506 | + |
507 | + >>> ignore = app.run() |
508 | + Launchpad credential client (console) |
509 | + ------------------------------------- |
510 | + An application identified as "consumer"... |
511 | + [If this were a real application, the end-user's web browser...] |
512 | + OK, you'll need to get yourself a Launchpad account before... |
513 | + <BLANKLINE> |
514 | + I'm opening the Launchpad registration page in your web browser... |
515 | + Press enter to go back to "consumer". [User input: ] |
516 | + Application exited with code -1 |
517 | + |
518 | +If the user keeps entering bad passwords, the app eventually gives up. |
519 | + |
520 | + >>> input_bad_password = UserInput( |
521 | + ... [username, "badpw", "", "badpw", "", "badpw", ""]) |
522 | + >>> json = simplejson.loads(token_app.run()) |
523 | + >>> request_token = json['oauth_token'] |
524 | + >>> app.request_token = request_token |
525 | + >>> app.input_method = input_bad_password |
526 | + >>> ignore = app.run() |
527 | + Launchpad credential client (console) |
528 | + ------------------------------------- |
529 | + An application identified as "consumer"... |
530 | + What email address do you use on Launchpad? |
531 | + (No Launchpad account? Just hit enter.) [User input: salgado@ubuntu.com] |
532 | + What's your Launchpad password? [User input: badpw] |
533 | + I can't log in with the credentials you gave me. Let's try again. |
534 | + What email address do you use on Launchpad? [salgado@ubuntu.com] [User input: ] |
535 | + What's your Launchpad password? [User input: badpw] |
536 | + I can't log in with the credentials you gave me. Let's try again. |
537 | + What email address do you use on Launchpad? [salgado@ubuntu.com] [User input: ] |
538 | + What's your Launchpad password? [User input: badpw] |
539 | + You've failed the password entry too many times... |
540 | + Press enter to go back to "consumer". [User input: ] |
541 | + Application exited with code -1 |
542 | + |
543 | |
544 | === modified file 'src/launchpadlib/testing/helpers.py' |
545 | --- src/launchpadlib/testing/helpers.py 2009-10-30 14:48:21 +0000 |
546 | +++ src/launchpadlib/testing/helpers.py 2009-10-30 19:21:14 +0000 |
547 | @@ -119,11 +119,36 @@ |
548 | print suggested_message |
549 | return self.password |
550 | |
551 | - def input_access_level(self, available_levels, suggested_message): |
552 | + def input_access_level(self, available_levels, suggested_message, |
553 | + only_one_option): |
554 | """Collect the desired level of access from the end-user.""" |
555 | print suggested_message |
556 | print [level['value'] for level in available_levels] |
557 | return self.choose_access_level |
558 | |
559 | - def startup(self, suggested_message): |
560 | - print suggested_message |
561 | + def startup(self, suggested_messages): |
562 | + for message in suggested_messages: |
563 | + print message |
564 | + |
565 | + |
566 | +class UserInput(object): |
567 | + """A class to store fake user input in a readable way. |
568 | + |
569 | + An instance of this class can be used as a substitute for the |
570 | + raw_input() function. |
571 | + """ |
572 | + |
573 | + def __init__(self, inputs): |
574 | + """Initialize with a line of user inputs.""" |
575 | + self.stream = iter(inputs) |
576 | + |
577 | + def __call__(self, prompt): |
578 | + """Print and return the next line of input.""" |
579 | + line = self.readline() |
580 | + print prompt + "[User input: %s]" % line |
581 | + return line |
582 | + |
583 | + def readline(self): |
584 | + """Return the next line of input.""" |
585 | + next_input = self.stream.next() |
586 | + return str(next_input) |
This branch does some refactoring and adds some more tests for the RequestTokenAut horizationEngin e, the core of any trusted request token authorization client other than the end-user's web browser.
1. The class itself is renamed from AuthorizeReques tTokenProcess to RequestTokenAut horizationEngin e. RTAE is not the best name but I found ARTP really confusing.
2. RTAE no longer overwrites 'allow_ access_ levels' after it gets the token information. This lets me use the same RTAE object twice in tests.
3. Added code to detect when Launchpad doesn't recognize the request token.
4. Gave default implementations for a lot of the RTAE plugin methods, which just print the suggested_message. I already defined this code in the ScriptableReque stTokenAuthoriz ation test helper, and I don't want to define it again for the command-line client.
5. Added tests for trying to approve a token that was already approved, trying to approve a nonexistent token, and triggering a server error.