Merge lp:~leonardr/launchpadlib/trusted-workflow-tests-2 into lp:launchpadlib

Proposed by Leonard Richardson
Status: Merged
Approved by: Gary Poster
Approved revision: 78
Merged at revision: not available
Proposed branch: lp:~leonardr/launchpadlib/trusted-workflow-tests-2
Merge into: lp:launchpadlib
Diff against target: 584 lines
5 files modified
src/launchpadlib/apps.py (+129/-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-2
Reviewer Review Type Date Requested Status
Gary Poster Approve
Review via email: mp+14230@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch creates a TrustedTokenAuthorizationConsoleApp, a console application that guides the end-user through the process of authorizing a request token. It creates a lot of doctests for the App object and a small command-line script that invokes it. The doctests use the new UserInput class which simulates a user typing things into a console application.

There are a lot of small spacing tweaks in this branch to make the application look good when run from the command line. These changes don't necessarily show up in the doctests, which are much more lenient about whitespace.

In addition to the doctests I manually ran the console application to test things that can't be automatically tested such as the use of getpass and the opening of a page in the web browser.

77. By Leonard Richardson

Merged with trunk.

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (7.6 KiB)

On Fri, 2009-10-30 at 17:35 +0000, Leonard Richardson wrote:
> Leonard Richardson has proposed merging lp:~leonardr/launchpadlib/trusted-workflow-tests-2 into lp:launchpadlib.
>
> Requested reviews:
> LAZR Developers (lazr-developers)
>
>
> This branch creates a TrustedTokenAuthorizationConsoleApp, a console
> application that guides the end-user through the process of
> authorizing a request token. It creates a lot of doctests for the App
> object and a small command-line script that invokes it. The doctests
> use the new UserInput class which simulates a user typing things into
> a console application.
>
> There are a lot of small spacing tweaks in this branch to make the
> application look good when run from the command line. These changes
> don't necessarily show up in the doctests, which are much more lenient
> about whitespace.

Did you consider changing your tests to run without the
NORMALIZE_WHITESPACE option, which is enabled by default for all our
tests? But I guess there's no real benefit in making the test more
fragile just to show that the app looks good when running on the command
line.

>
> In addition to the doctests I manually ran the console application to
> test things that can't be automatically tested such as the use of
> getpass and the opening of a page in the web browser.
> --

> === modified file 'src/launchpadlib/apps.py'
> --- src/launchpadlib/apps.py 2009-10-28 17:27:53 +0000
> +++ src/launchpadlib/apps.py 2009-10-30 17:35:17 +0000
> @@ -20,9 +20,18 @@
> themselves are kept in bin/.
> """
>
> +__all__ = [
> + 'RequestTokenApp',
> + 'TrustedTokenAuthorizationConsoleApp',
> + ]
> +
> +import getpass
> +import sys
> +
> import simplejson
>
> -from launchpadlib.credentials import Credentials
> +from launchpadlib.credentials import (
> + Credentials, RequestTokenAuthorizationEngine, TokenAuthorizationException)
> from launchpadlib.uris import lookup_web_root
>
>
> @@ -43,3 +52,124 @@
> return simplejson.dumps(token)
>
>
> +class TrustedTokenAuthorizationConsoleApp(RequestTokenAuthorizationEngine):
> + """An application that authorizes request tokens."""
> +
> + def __init__(self, web_root, consumer_name, request_token,
> + access_levels='', input_method=raw_input):
> + """Constructor.
> +
> + :param access_levels: A string of comma-separated access level
> + values. To get an up-to-date list of access levels, pass
> + token_format=Credentials.DICT_TOKEN_FORMAT into
> + Credentials.get_request_token, load the dict as JSON, and look
> + in 'access_levels'.
> + """
> + access_levels = [level.strip() for level in access_levels.split(',')]
> + super(TrustedTokenAuthorizationConsoleApp, self).__init__(
> + web_root, consumer_name, request_token, access_levels)
> +
> + self.input_method = input_method
> +
> + def run(self):
> + """Try to authorize a request token from user input."""
> + self.error_code = -1 # Start off assuming failure.
> + start = "Launchpad credential client (console)"
> + self.output(start)
> + self.output("-" ...

Read more...

Revision history for this message
Guilherme Salgado (salgado) wrote :

On Fri, 2009-10-30 at 17:29 -0200, Guilherme Salgado wrote:
>
> > + return self.pressEnterToExit()
> > +
> > + def exit_with(self, code):
>
> Why is this not exitWith()?

As you explained to me on IRC, this is because you're using the python
naming standard instead of the LP one. Fair enough, but the two methods
below use the LP one. Care to rename them?

And here's a review of the two remaining files, which had conflicts when
I first reviewed your branch. Just one question, in fact.

> === modified file 'src/launchpadlib/credentials.py'
> --- src/launchpadlib/credentials.py 2009-10-30 14:48:21 +0000
> +++ src/launchpadlib/credentials.py 2009-10-30 19:55:30 +0000
> @@ -289,16 +291,20 @@
>
> def __init__(self, web_root, consumer_name, request_token,
> allow_access_levels=[], max_failed_attempts=3):
> - self.web_root = web_root
> + self.web_root = uris.lookup_web_root(web_root)

What's the reason for this change?

> self.consumer_name = consumer_name
> self.request_token = request_token
> self.browser = SimulatedLaunchpadBrowser(self.web_root)
> self.max_failed_attempts = max_failed_attempts
> self.allow_access_levels = allow_access_levels
> + self.text_wrapper = textwrap.TextWrapper(
> + replace_whitespace=False, width=78)
>

Revision history for this message
Leonard Richardson (leonardr) wrote :

> > + try:
> > + self()
> > + except TokenAuthorizationException, e:
> > + print str(e)
> > + self.error_code = -1
> > + return self.pressEnterToExit()
>
> The above call is redundant because of the one below, no?
>
> > + return self.pressEnterToExit()

Yes, I've removed the redundant call.

> > + def pressEnterToExit(self):
> > + """Make the user hit enter, and then exit with an error code."""
> > + prompt = '\nPress enter to go back to "%s". ' % self.consumer_name
> > + self.input_method(prompt)
> > + self.exit_with(self.error_code)
> > + return self.error_code
>
> Since exit_with() will call sys.exit(), there's no reason to return
> anything here, is there?

The default implementation of exit_with() calls sys.exit(), but my testing version returned the error code. However I was just ignoring the error code in my tests, so it's not necessary.

Revision history for this message
Leonard Richardson (leonardr) wrote :

> As you explained to me on IRC, this is because you're using the python
> naming standard instead of the LP one. Fair enough, but the two methods
> below use the LP one. Care to rename them?

I've renamed getSingleCharInput and pressEnterToExit.

> And here's a review of the two remaining files, which had conflicts when
> I first reviewed your branch. Just one question, in fact.
>
> > === modified file 'src/launchpadlib/credentials.py'
> > --- src/launchpadlib/credentials.py 2009-10-30 14:48:21 +0000
> > +++ src/launchpadlib/credentials.py 2009-10-30 19:55:30 +0000
> > @@ -289,16 +291,20 @@
> >
> > def __init__(self, web_root, consumer_name, request_token,
> > allow_access_levels=[], max_failed_attempts=3):
> > - self.web_root = web_root
> > + self.web_root = uris.lookup_web_root(web_root)
>
> What's the reason for this change?

I think I explained this on IRC too, but for the record: lookup_web_root lets you pass in "staging" as the site root instead of "https://staging.launchpad.net/".

78. By Leonard Richardson

Response to feedback.

Revision history for this message
Gary Poster (gary) wrote :

Approving on Salgado's behalf because Leonard appears to have addressed his concerns and Salgado is not available today.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/launchpadlib/apps.py'
--- src/launchpadlib/apps.py 2009-10-28 17:27:53 +0000
+++ src/launchpadlib/apps.py 2009-11-02 12:35:19 +0000
@@ -20,9 +20,18 @@
20themselves are kept in bin/.20themselves are kept in bin/.
21"""21"""
2222
23__all__ = [
24 'RequestTokenApp',
25 'TrustedTokenAuthorizationConsoleApp',
26 ]
27
28import getpass
29import sys
30
23import simplejson31import simplejson
2432
25from launchpadlib.credentials import Credentials33from launchpadlib.credentials import (
34 Credentials, RequestTokenAuthorizationEngine, TokenAuthorizationException)
26from launchpadlib.uris import lookup_web_root35from launchpadlib.uris import lookup_web_root
2736
2837
@@ -43,3 +52,122 @@
43 return simplejson.dumps(token)52 return simplejson.dumps(token)
4453
4554
55class TrustedTokenAuthorizationConsoleApp(RequestTokenAuthorizationEngine):
56 """An application that authorizes request tokens."""
57
58 def __init__(self, web_root, consumer_name, request_token,
59 access_levels='', input_method=raw_input):
60 """Constructor.
61
62 :param access_levels: A string of comma-separated access level
63 values. To get an up-to-date list of access levels, pass
64 token_format=Credentials.DICT_TOKEN_FORMAT into
65 Credentials.get_request_token, load the dict as JSON, and look
66 in 'access_levels'.
67 """
68 access_levels = [level.strip() for level in access_levels.split(',')]
69 super(TrustedTokenAuthorizationConsoleApp, self).__init__(
70 web_root, consumer_name, request_token, access_levels)
71
72 self.input_method = input_method
73
74 def run(self):
75 """Try to authorize a request token from user input."""
76 self.error_code = -1 # Start off assuming failure.
77 start = "Launchpad credential client (console)"
78 self.output(start)
79 self.output("-" * len(start))
80
81 try:
82 self()
83 except TokenAuthorizationException, e:
84 print str(e)
85 self.error_code = -1
86 return self.press_enter_to_exit()
87
88 def exit_with(self, code):
89 """Exit the app with the specified error code."""
90 sys.exit(code)
91
92 def get_single_char_input(self, prompt, valid):
93 """Retrieve a single-character line from the input stream."""
94 valid = valid.upper()
95 input = None
96 while input is None:
97 input = self.input_method(prompt).upper()
98 if len(input) != 1 or input not in valid:
99 input = None
100 return input
101
102 def press_enter_to_exit(self):
103 """Make the user hit enter, and then exit with an error code."""
104 prompt = '\nPress enter to go back to "%s". ' % self.consumer_name
105 self.input_method(prompt)
106 self.exit_with(self.error_code)
107
108 def input_username(self, cached_username, suggested_message):
109 """Collect the Launchpad username from the end-user.
110
111 :param cached_username: A username from a previous entry attempt,
112 to be presented as the default.
113 """
114 if cached_username is not None:
115 extra = " [%s] " % cached_username
116 else:
117 extra = "\n(No Launchpad account? Just hit enter.) "
118 username = self.input_method(suggested_message + extra)
119 if username == '':
120 return cached_username
121 return username
122
123 def input_password(self, suggested_message):
124 """Collect the Launchpad password from the end-user."""
125 if self.input_method is raw_input:
126 password = getpass.getpass(suggested_message + " ")
127 else:
128 password = self.input_method(suggested_message)
129 return password
130
131 def input_access_level(self, available_levels, suggested_message,
132 only_one_option=None):
133 """Collect the desired level of access from the end-user."""
134 if only_one_option is not None:
135 self.output(suggested_message)
136 prompt = self.message(
137 'Do you want to give "%(app)s" this level of access? [YN] ')
138 allow = self.get_single_char_input(prompt, "YN")
139 if allow == "Y":
140 return only_one_option['value']
141 else:
142 return self.UNAUTHORIZED_ACCESS_LEVEL
143 else:
144 levels_except_unauthorized = [
145 level for level in available_levels
146 if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL]
147 options = []
148 for i in range(0, len(levels_except_unauthorized)):
149 options.append(
150 "%d: %s" % (i+1, levels_except_unauthorized[i]['title']))
151 self.output(suggested_message)
152 for option in options:
153 self.output(option)
154 allowed = ("".join(map(str, range(1, i+2)))) + "Q"
155 prompt = self.message(
156 'What should "%(app)s" be allowed to do using your '
157 'Launchpad account? [1-%(max)d or Q] ',
158 extra_variables = {'max' : i+1})
159 allow = self.get_single_char_input(prompt, allowed)
160 if allow == "Q":
161 return self.UNAUTHORIZED_ACCESS_LEVEL
162 else:
163 return levels_except_unauthorized[int(allow)-1]['value']
164
165 def user_refused_to_authorize(self, suggested_message):
166 """The user refused to authorize a request token."""
167 self.output(suggested_message)
168 self.error_code = -2
169
170 def user_authorized(self, access_level, suggested_message):
171 """The user authorized a request token with some access level."""
172 self.output(suggested_message)
173 self.error_code = 0
46174
=== added file 'src/launchpadlib/bin/launchpad-credentials-console'
--- src/launchpadlib/bin/launchpad-credentials-console 1970-01-01 00:00:00 +0000
+++ src/launchpadlib/bin/launchpad-credentials-console 2009-11-02 12:35:19 +0000
@@ -0,0 +1,54 @@
1#!/usr/bin/python
2
3# Copyright 2009 Canonical Ltd.
4
5# This file is part of launchpadlib.
6#
7# launchpadlib is free software: you can redistribute it and/or modify it
8# under the terms of the GNU Lesser General Public License as published by the
9# Free Software Foundation, version 3 of the License.
10#
11# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
14# for more details.
15#
16# You should have received a copy of the GNU Lesser General Public License
17# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
18
19"""Take the user's Launchpad credentials and authorize a request token.
20
21This script will guide the user through the process of authorizing a
22request token with their username and password. It's not as secure as
23opening the user's web browser, but if you are writing a console
24application and want to keep the authorization process console-based,
25it's better to use this script than write the authorization code yourself.
26"""
27
28__metaclass__ = type
29
30from optparse import OptionParser
31from launchpadlib.apps import (
32 RequestTokenApp, TrustedTokenAuthorizationConsoleApp)
33
34parser = OptionParser()
35parser.usage = "%prog CONSUMER_NAME REQUEST_TOKEN [-r URL] [-a LEVEL,LEVEL,...]"
36parser.add_option("-s", "--site", dest="web_root",
37 help=("The name of the Launchpad site on which the request "
38 "token can be found (default: %default). This can "
39 "also be the URL to the root of the site."),
40 metavar="URL", default="staging")
41parser.add_option("-a", "--access-level", dest="access_levels",
42 help="Any restrictions on your clients' access levels",
43 metavar="[LEVEL,LEVEL,...]", default="")
44
45if __name__ == '__main__':
46 (options, args) = parser.parse_args()
47 if len(args) < 2:
48 parser.error("You must provide both a consumer name and the ID of "
49 "an OAuth request token on Launchpad.")
50 consumer_name, token_id = args[:2]
51 TrustedTokenAuthorizationConsoleApp(
52 options.web_root, consumer_name, token_id,
53 options.access_levels).run()
54
055
=== modified file 'src/launchpadlib/credentials.py'
--- src/launchpadlib/credentials.py 2009-10-30 14:48:21 +0000
+++ src/launchpadlib/credentials.py 2009-11-02 12:35:19 +0000
@@ -27,8 +27,10 @@
27import base6427import base64
28import cgi28import cgi
29import httplib229import httplib2
30import textwrap
30from urllib import urlencode, quote31from urllib import urlencode, quote
31from urlparse import urljoin32from urlparse import urljoin
33import webbrowser
3234
33import simplejson35import simplejson
3436
@@ -265,7 +267,7 @@
265267
266 INPUT_USERNAME = "What email address do you use on Launchpad?"268 INPUT_USERNAME = "What email address do you use on Launchpad?"
267269
268 INPUT_PASSWORD = "What's your Launchpad password?"270 INPUT_PASSWORD = "What's your Launchpad password? "
269271
270 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."""272 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."""
271273
@@ -273,9 +275,9 @@
273275
274 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"."""276 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"."""
275277
276 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.278 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."""
277279
278I'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."""280 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."""
279281
280 SUCCESS = """You're all done! You should now be able to use Launchpad integration features of "%(app)s." """282 SUCCESS = """You're all done! You should now be able to use Launchpad integration features of "%(app)s." """
281283
@@ -289,16 +291,20 @@
289291
290 def __init__(self, web_root, consumer_name, request_token,292 def __init__(self, web_root, consumer_name, request_token,
291 allow_access_levels=[], max_failed_attempts=3):293 allow_access_levels=[], max_failed_attempts=3):
292 self.web_root = web_root294 self.web_root = uris.lookup_web_root(web_root)
293 self.consumer_name = consumer_name295 self.consumer_name = consumer_name
294 self.request_token = request_token296 self.request_token = request_token
295 self.browser = SimulatedLaunchpadBrowser(self.web_root)297 self.browser = SimulatedLaunchpadBrowser(self.web_root)
296 self.max_failed_attempts = max_failed_attempts298 self.max_failed_attempts = max_failed_attempts
297 self.allow_access_levels = allow_access_levels299 self.allow_access_levels = allow_access_levels
300 self.text_wrapper = textwrap.TextWrapper(
301 replace_whitespace=False, width=78)
298302
299 def __call__(self):303 def __call__(self):
300304
301 self.startup(self.message(self.STARTUP_MESSAGE))305 self.startup(
306 [self.message(self.STARTUP_MESSAGE),
307 self.message(self.STARTUP_MESSAGE_2)])
302308
303 # Have the end-user enter their Launchpad username and password.309 # Have the end-user enter their Launchpad username and password.
304 # Make sure the credentials are valid, and get information310 # Make sure the credentials are valid, and get information
@@ -315,14 +321,17 @@
315 # There's only one choice: allow access at a certain level321 # There's only one choice: allow access at a certain level
316 # or don't allow access at all.322 # or don't allow access at all.
317 message = self.CHOOSE_ACCESS_LEVEL_ONE323 message = self.CHOOSE_ACCESS_LEVEL_ONE
318 level = [level['title'] for level in self.reconciled_access_levels324 level = [level for level in self.reconciled_access_levels
319 if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL]325 if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL][0]
320 extra = {'level' : level[0]}326 extra = {'level' : level['title']}
327 only_one_option = level
321 else:328 else:
322 message = self.CHOOSE_ACCESS_LEVEL329 message = self.CHOOSE_ACCESS_LEVEL
323 extra = None330 extra = None
331 only_one_option = None
324 access_level = self.input_access_level(332 access_level = self.input_access_level(
325 self.reconciled_access_levels, self.message(message, extra))333 self.reconciled_access_levels, self.message(message, extra),
334 only_one_option)
326335
327 # Notify the program of the user's choice.336 # Notify the program of the user's choice.
328 if access_level == self.UNAUTHORIZED_ACCESS_LEVEL:337 if access_level == self.UNAUTHORIZED_ACCESS_LEVEL:
@@ -420,8 +429,8 @@
420429
421 # You should define these methods in your subclass.430 # You should define these methods in your subclass.
422431
423 def startup(self, suggested_message):432 def output(self, message):
424 """Hook method called on startup."""433 print self.text_wrapper.fill(message)
425434
426 def input_username(self, cached_username, suggested_message):435 def input_username(self, cached_username, suggested_message):
427 """Collect the Launchpad username from the end-user.436 """Collect the Launchpad username from the end-user.
@@ -435,22 +444,31 @@
435 """Collect the Launchpad password from the end-user."""444 """Collect the Launchpad password from the end-user."""
436 raise NotImplementedError()445 raise NotImplementedError()
437446
438 def input_access_level(self, available_levels, suggested_message):447 def input_access_level(self, available_levels, suggested_message,
448 only_one_option=None):
439 """Collect the desired level of access from the end-user."""449 """Collect the desired level of access from the end-user."""
440 raise NotImplementedError()450 raise NotImplementedError()
441451
452 def startup(self, suggested_messages):
453 """Hook method called on startup."""
454 for message in suggested_messages:
455 self.output(message)
456 self.output("\n")
442457
443 def authentication_failure(self, suggested_message):458 def authentication_failure(self, suggested_message):
444 """The user entered invalid credentials."""459 """The user entered invalid credentials."""
445 print suggested_message460 self.output(suggested_message)
461 self.output("\n")
446462
447 def user_refused_to_authorize(self, suggested_message):463 def user_refused_to_authorize(self, suggested_message):
448 """The user refused to authorize a request token."""464 """The user refused to authorize a request token."""
449 print suggested_message465 self.output(suggested_message)
466 self.output("\n")
450467
451 def user_authorized(self, access_level, suggested_message):468 def user_authorized(self, access_level, suggested_message):
452 """The user authorized a request token with some access level."""469 """The user authorized a request token with some access level."""
453 print suggested_message470 self.output(suggested_message)
471 self.output("\n")
454472
455 def server_consumer_differs_from_client_consumer(473 def server_consumer_differs_from_client_consumer(
456 self, client_name, real_name, suggested_message):474 self, client_name, real_name, suggested_message):
@@ -461,11 +479,13 @@
461 end-user that its name is "bar". Something is fishy and at the very479 end-user that its name is "bar". Something is fishy and at the very
462 least the end-user should be warned about this.480 least the end-user should be warned about this.
463 """481 """
464 print suggested_message482 self.output("\n")
483 self.output(suggested_message)
484 self.output("\n")
465485
466 def success(self, suggested_message):486 def success(self, suggested_message):
467 """The token was successfully authorized."""487 """The token was successfully authorized."""
468 print suggested_message488 self.output(suggested_message)
469489
470490
471class TokenAuthorizationException(Exception):491class TokenAuthorizationException(Exception):
472492
=== modified file 'src/launchpadlib/docs/command-line.txt'
--- src/launchpadlib/docs/command-line.txt 2009-10-27 16:26:06 +0000
+++ src/launchpadlib/docs/command-line.txt 2009-11-02 12:35:19 +0000
@@ -20,9 +20,11 @@
2020
21 >>> import simplejson21 >>> import simplejson
22 >>> from launchpadlib.apps import RequestTokenApp22 >>> from launchpadlib.apps import RequestTokenApp
23 >>> app = RequestTokenApp(23
24 ... "http://launchpad.dev:8085/", "consumer", "context")24 >>> web_root = "http://launchpad.dev:8085/"
25 >>> json = simplejson.loads(app.run())25 >>> consumer_name = "consumer"
26 >>> token_app = RequestTokenApp(web_root, consumer_name, "context")
27 >>> json = simplejson.loads(token_app.run())
2628
27 >>> sorted(json.keys())29 >>> sorted(json.keys())
28 ['access_levels', 'lp.context', 'oauth_token',30 ['access_levels', 'lp.context', 'oauth_token',
@@ -33,3 +35,171 @@
3335
34 >>> print json['oauth_token_consumer']36 >>> print json['oauth_token_consumer']
35 consumer37 consumer
38
39TrustedTokenAuthorizationConsoleApp
40===================================
41
42This class is called by the command-line script
43launchpad-credentials-console. It asks for the user's Launchpad
44username and password, and authorizes a request token on their
45behalf.
46
47 >>> from launchpadlib.apps import TrustedTokenAuthorizationConsoleApp
48 >>> from launchpadlib.testing.helpers import UserInput
49
50This class does not create the request token, or exchange it for the
51access token--that's the job of the program that calls
52launchpad-credentials-console. So we'll use the request token created
53earlier by RequestTokenApp.
54
55 >>> request_token = json['oauth_token']
56
57Since this is a test, we don't want the application to call sys.exit()
58or try to open up pages in a web browser. This subclass of
59TrustedTokenAuthorizationConsoleApp will print messages instead of
60performing such un-doctest-like actions.
61
62 >>> class ConsoleApp(TrustedTokenAuthorizationConsoleApp):
63 ... def open_login_page_in_user_browser(self, url):
64 ... """Print a status message."""
65 ... self.output("[If this were a real application, the "
66 ... "end-user's web browser would be opened "
67 ... "to %s]" % url)
68 ...
69 ... def exit_with(self, code):
70 ... print "Application exited with code %d" % code
71
72We'll use a UserInput object to simulate a user typing things in at
73the prompt and hitting enter. This UserInput runs the program
74correctly, entering the Launchpad username and password, choosing an
75access level, and hitting Enter to exit.
76
77 >>> username = "salgado@ubuntu.com"
78 >>> password = "zeca"
79 >>> fake_input = UserInput([username, password, "1", ""])
80
81Here's a successful run of the application. When the request token is
82authorized, the script's response code is 0.
83
84 >>> app = ConsoleApp(
85 ... web_root, consumer_name, request_token,
86 ... 'READ_PRIVATE, READ_PUBLIC', input_method=fake_input)
87 >>> app.run()
88 Launchpad credential client (console)
89 -------------------------------------
90 An application identified as "consumer" wants to access Launchpad...
91 What email address do you use on Launchpad?
92 (No Launchpad account? Just hit enter.) [User input: salgado@ubuntu.com]
93 What's your Launchpad password? [User input: zeca]
94 Now it's time for you to decide how much power to give "consumer"...
95 1: Read Non-Private Data
96 2: Read Anything
97 What should "consumer" be allowed to do...? [1-2 or Q] [User input: 1]
98 Okay, I'm telling Launchpad to grant "consumer" access to your account.
99 You're all done!...
100 Press enter to go back to "consumer". [User input: ]
101 Application exited with code 0
102
103Now that the request token has been authorized, we'll need to create
104another one to continue the test.
105
106 >>> json = simplejson.loads(token_app.run())
107 >>> request_token = json['oauth_token']
108 >>> app.request_token = request_token
109
110Invalid input is ignored. The user may enter 'Q' instead of a number
111to refuse to authorize the request token. When the user denies access,
112the exit code is -2.
113
114 >>> fake_input = UserInput([username, password, "A", "99", "Q", ""])
115 >>> app.input_method = fake_input
116 >>> app.run()
117 Launchpad credential client (console)
118 -------------------------------------
119 An application identified as "consumer"...
120 What should "consumer" be allowed to do...? [1-2 or Q] [User input: A]
121 What should "consumer" be allowed to do...? [1-2 or Q] [User input: 99]
122 What should "consumer" be allowed to do...? [1-2 or Q] [User input: Q]
123 Okay, I'm going to cancel the request...
124 You're all done! "consumer" still doesn't have access...
125 <BLANKLINE>
126 Press enter to go back to "consumer". [User input: ]
127 Application exited with code -2
128
129When the third-party application will allow only one level of access,
130the end-user is presented with a yes-or-no choice instead of a list to
131choose from. Again, invalid input is ignored.
132
133 >>> json = simplejson.loads(token_app.run())
134 >>> request_token = json['oauth_token']
135 >>> fake_input = UserInput([username, password, "1", "Q", "Y", ""])
136
137 >>> app = ConsoleApp(
138 ... web_root, consumer_name, request_token,
139 ... 'READ_PRIVATE', input_method=fake_input)
140
141 >>> app.run()
142 Launchpad credential client (console)
143 -------------------------------------
144 An application identified as "consumer"...
145 Do you want to give "consumer" this level of access? [YN] [User input: 1]
146 Do you want to give "consumer" this level of access? [YN] [User input: Q]
147 Do you want to give "consumer" this level of access? [YN] [User input: Y]
148 ...
149 Application exited with code 0
150
151
152Error handling
153--------------
154
155When the end-user refuses to authorize the request token, the app
156exits with a return code of -2, as seen above. When any other error
157gets in the way of the authorization of the request token, the app's
158return code is -1.
159
160If the user hits enter when asked for their email address, indicating
161that they don't have a Launchpad account, the app opens their browser
162to the Launchpad login page.
163
164 >>> json = simplejson.loads(token_app.run())
165 >>> app.request_token = json['oauth_token']
166
167 >>> input_nothing = UserInput(["", ""])
168 >>> app.input_method = input_nothing
169
170 >>> app.run()
171 Launchpad credential client (console)
172 -------------------------------------
173 An application identified as "consumer"...
174 [If this were a real application, the end-user's web browser...]
175 OK, you'll need to get yourself a Launchpad account before...
176 <BLANKLINE>
177 I'm opening the Launchpad registration page in your web browser...
178 Press enter to go back to "consumer". [User input: ]
179 Application exited with code -1
180
181If the user keeps entering bad passwords, the app eventually gives up.
182
183 >>> input_bad_password = UserInput(
184 ... [username, "badpw", "", "badpw", "", "badpw", ""])
185 >>> json = simplejson.loads(token_app.run())
186 >>> request_token = json['oauth_token']
187 >>> app.request_token = request_token
188 >>> app.input_method = input_bad_password
189 >>> app.run()
190 Launchpad credential client (console)
191 -------------------------------------
192 An application identified as "consumer"...
193 What email address do you use on Launchpad?
194 (No Launchpad account? Just hit enter.) [User input: salgado@ubuntu.com]
195 What's your Launchpad password? [User input: badpw]
196 I can't log in with the credentials you gave me. Let's try again.
197 What email address do you use on Launchpad? [salgado@ubuntu.com] [User input: ]
198 What's your Launchpad password? [User input: badpw]
199 I can't log in with the credentials you gave me. Let's try again.
200 What email address do you use on Launchpad? [salgado@ubuntu.com] [User input: ]
201 What's your Launchpad password? [User input: badpw]
202 You've failed the password entry too many times...
203 Press enter to go back to "consumer". [User input: ]
204 Application exited with code -1
205
36206
=== modified file 'src/launchpadlib/testing/helpers.py'
--- src/launchpadlib/testing/helpers.py 2009-10-30 14:48:21 +0000
+++ src/launchpadlib/testing/helpers.py 2009-11-02 12:35:19 +0000
@@ -119,11 +119,36 @@
119 print suggested_message119 print suggested_message
120 return self.password120 return self.password
121121
122 def input_access_level(self, available_levels, suggested_message):122 def input_access_level(self, available_levels, suggested_message,
123 only_one_option):
123 """Collect the desired level of access from the end-user."""124 """Collect the desired level of access from the end-user."""
124 print suggested_message125 print suggested_message
125 print [level['value'] for level in available_levels]126 print [level['value'] for level in available_levels]
126 return self.choose_access_level127 return self.choose_access_level
127128
128 def startup(self, suggested_message):129 def startup(self, suggested_messages):
129 print suggested_message130 for message in suggested_messages:
131 print message
132
133
134class UserInput(object):
135 """A class to store fake user input in a readable way.
136
137 An instance of this class can be used as a substitute for the
138 raw_input() function.
139 """
140
141 def __init__(self, inputs):
142 """Initialize with a line of user inputs."""
143 self.stream = iter(inputs)
144
145 def __call__(self, prompt):
146 """Print and return the next line of input."""
147 line = self.readline()
148 print prompt + "[User input: %s]" % line
149 return line
150
151 def readline(self):
152 """Return the next line of input."""
153 next_input = self.stream.next()
154 return str(next_input)

Subscribers

People subscribed via source and target branches