Merge lp:~nataliabidart/ubuntu-sso-client/handle-user-not-validated into lp:ubuntu-sso-client

Proposed by Natalia Bidart
Status: Merged
Approved by: Natalia Bidart
Approved revision: 647
Merged at revision: 646
Proposed branch: lp:~nataliabidart/ubuntu-sso-client/handle-user-not-validated
Merge into: lp:ubuntu-sso-client
Diff against target: 447 lines (+181/-24)
6 files modified
ubuntu_sso/account.py (+21/-0)
ubuntu_sso/gui.py (+17/-2)
ubuntu_sso/main.py (+31/-18)
ubuntu_sso/tests/test_account.py (+37/-0)
ubuntu_sso/tests/test_gui.py (+37/-1)
ubuntu_sso/tests/test_main.py (+38/-3)
To merge this branch: bzr merge lp:~nataliabidart/ubuntu-sso-client/handle-user-not-validated
Reviewer Review Type Date Requested Status
Alejandro J. Cura (community) Approve
Roman Yepishev (community) fieldtest Approve
Review via email: mp+39779@code.launchpad.net

Commit message

* Added a new DBus signal UserNotValidated to indicate when a user is registered but not validated (LP: #667899).
* Added new workflow so email validation is requested if necessary.

Description of the change

To test, run in this branch the following:

* killall ubuntu-sso-login; DEBUG=True PYTHONPATH=. ./bin/ubuntu-sso-login

* In d-feet, execute the method 'register' in the com.ubuntu.sso bus name, object path /com/ubuntu/sso/credentials, interface name CredentialsManagement.
Parameters must be something like:

'Ubuntu One', {'ping_url': 'https://edge.one.ubuntu.com/oauth/sso-finished-so-get-tokens/'}

* Once you get the SSO GUI, register a non existent user and do not enter the validation code. Just close the window when the validation code is requested.

* Open the SSO GUI again using d-feet and try to register the same user, you'll get a "Email already registered" error.

* Click on "I already have an account..." to login, and try to login with the former user and pass.

Behavior expected:

 - The login process doesn't succeed but instead the verification screen appears.

To post a comment you must log in.
Revision history for this message
Roman Yepishev (rye) wrote :

Works as advertised.
Just awesome!

review: Approve (fieldtest)
Revision history for this message
Alejandro J. Cura (alecu) wrote :

Looks great!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ubuntu_sso/account.py'
2--- ubuntu_sso/account.py 2010-10-01 17:07:25 +0000
3+++ ubuntu_sso/account.py 2010-11-01 20:39:44 +0000
4@@ -170,6 +170,27 @@
5 'token_name: %r', credentials['consumer_key'], token_name)
6 return credentials
7
8+ def is_validated(self, email, password, token_name, sso_service=None):
9+ """Return if user with 'email' and 'password' is validated."""
10+ if sso_service is None:
11+ token = self.login(email=email, password=password,
12+ token_name=token_name)
13+
14+ oauth_token = oauth.OAuthToken(token['token'],
15+ token['token_secret'])
16+ authorizer = OAuthAuthorizer(token['consumer_key'],
17+ token['consumer_secret'],
18+ oauth_token)
19+ sso_service = self.sso_service_class(authorizer, self.service_url)
20+
21+ me_info = sso_service.accounts.me()
22+ key = 'preferred_email'
23+ result = key in me_info and me_info[key] != None
24+
25+ logger.debug('is_validated: email: %r token_name: %r, result: %r.',
26+ email, token_name, result)
27+ return result
28+
29 def validate_email(self, email, password, email_token, token_name):
30 """Validate an email token for user with 'email' and 'password'."""
31 logger.debug('validate_email: email: %r password: <hidden>, '
32
33=== modified file 'ubuntu_sso/gui.py'
34--- ubuntu_sso/gui.py 2010-10-07 18:59:16 +0000
35+++ ubuntu_sso/gui.py 2010-11-01 20:39:44 +0000
36@@ -205,6 +205,8 @@
37 """Ubuntu single sign on GUI."""
38
39 CAPTCHA_SOLUTION_ENTRY = _('Type the characters above')
40+ CAPTCHA_LOAD_ERROR = _('There was a problem getting the captcha, '
41+ 'reloading...')
42 CONNECT_HELP_LABEL = _('To connect this computer to %(app_name)s ' \
43 'enter your details below.')
44 EMAIL1_ENTRY = _('Email address')
45@@ -270,6 +272,8 @@
46 self.tc_url = tc_url
47 self.help_text = help_text
48 self.close_callback = close_callback
49+ self.user_email = None
50+ self.user_password = None
51
52 ui_filename = get_data_file('ui.glade')
53 builder = gtk.Builder()
54@@ -373,6 +377,8 @@
55 self._filter_by_app_name(self.on_logged_in),
56 'LoginError':
57 self._filter_by_app_name(self.on_login_error),
58+ 'UserNotValidated':
59+ self._filter_by_app_name(self.on_user_not_validated),
60 'PasswordResetTokenSent':
61 self._filter_by_app_name(self.on_password_reset_token_sent),
62 'PasswordResetError':
63@@ -805,6 +811,8 @@
64 return
65
66 self._set_current_page(self.processing_vbox)
67+ self.user_email = email1
68+ self.user_password = password1
69
70 logger.info('Calling register_user with email %r, password <hidden>,' \
71 ' captcha_id %r and captcha_solution %r.', email1,
72@@ -825,8 +833,8 @@
73 self.email_token_entry.set_warning(self.FIELD_REQUIRED)
74 return
75
76- email = self.email1_entry.get_text()
77- password = self.password1_entry.get_text()
78+ email = self.user_email
79+ password = self.user_password
80 f = self.backend.validate_email
81 logger.info('Calling validate_email with email %r, password <hidden>' \
82 ', app_name %r and email_token %r.', email, self.app_name,
83@@ -864,6 +872,8 @@
84 reply_handler=NO_OP, error_handler=NO_OP)
85
86 self._set_current_page(self.processing_vbox)
87+ self.user_email = email
88+ self.user_password = password
89
90 def on_login_back_button_clicked(self, *args, **kwargs):
91 """User wants to go to the previous page."""
92@@ -1105,6 +1115,11 @@
93 self._append_error_signal(SIG_LOGIN_FAILED, error)
94
95 @log_call
96+ def on_user_not_validated(self, app_name, email, *args, **kwargs):
97+ """User was not validated."""
98+ self.on_user_registered(app_name, email)
99+
100+ @log_call
101 def on_password_reset_token_sent(self, app_name, email, *args, **kwargs):
102 """Password reset token was successfully sent."""
103 msg = self.SET_NEW_PASSWORD_LABEL % {'email': email}
104
105=== modified file 'ubuntu_sso/main.py'
106--- ubuntu_sso/main.py 2010-10-07 19:10:00 +0000
107+++ ubuntu_sso/main.py 2010-11-01 20:39:44 +0000
108@@ -106,12 +106,8 @@
109 dbus.service.Object.__init__(self, object_path=object_path,
110 bus_name=bus_name)
111 self.sso_login_processor_class = sso_login_processor_class
112- self.sso_service_class = sso_service_class
113-
114- def processor(self):
115- """Create a login processor with the given class and service class."""
116- return self.sso_login_processor_class(
117- sso_service_class=self.sso_service_class)
118+ self.processor = self.sso_login_processor_class(
119+ sso_service_class=sso_service_class)
120
121 # generate_capcha signals
122 @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")
123@@ -132,7 +128,7 @@
124 """Call the matching method in the processor."""
125 def f():
126 """Inner function that will be run in a thread."""
127- return self.processor().generate_captcha(filename)
128+ return self.processor.generate_captcha(filename)
129 blocking(f, app_name, self.CaptchaGenerated,
130 self.CaptchaGenerationError)
131
132@@ -156,8 +152,8 @@
133 """Call the matching method in the processor."""
134 def f():
135 """Inner function that will be run in a thread."""
136- return self.processor().register_user(email, password,
137- captcha_id, captcha_solution)
138+ return self.processor.register_user(email, password,
139+ captcha_id, captcha_solution)
140 blocking(f, app_name, self.UserRegistered, self.UserRegistrationError)
141
142 # login signals
143@@ -173,6 +169,12 @@
144 logger.debug('SSOLogin: emitting LoginError with '
145 'app_name "%s" and error %r', app_name, error)
146
147+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")
148+ def UserNotValidated(self, app_name, result):
149+ """Signal thrown when the user is not validated."""
150+ logger.debug('SSOLogin: emitting UserNotValidated with app_name "%s" '
151+ 'and result %r', app_name, result)
152+
153 @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
154 in_signature='sss')
155 def login(self, app_name, email, password):
156@@ -182,12 +184,23 @@
157 token_name = get_token_name(app_name)
158 logger.debug('login: token_name %r, email %r, password <hidden>.',
159 token_name, email)
160- credentials = self.processor().login(email, password, token_name)
161+ credentials = self.processor.login(email, password, token_name)
162 logger.debug('login returned not None credentials? %r.',
163 credentials is not None)
164- keyring_store_credentials(app_name, credentials)
165- return email
166- blocking(f, app_name, self.LoggedIn, self.LoginError)
167+ return credentials
168+
169+ def success_cb(app_name, credentials):
170+ """Login finished successfull."""
171+ token_name = get_token_name(app_name)
172+ is_validated = self.processor.is_validated(email, password,
173+ token_name)
174+ logger.debug('user is validated? %r.', is_validated)
175+ if is_validated:
176+ keyring_store_credentials(app_name, credentials)
177+ self.LoggedIn(app_name, email)
178+ else:
179+ self.UserNotValidated(app_name, email)
180+ blocking(f, app_name, success_cb, self.LoginError)
181
182 # validate_email signals
183 @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")
184@@ -209,8 +222,8 @@
185 def f():
186 """Inner function that will be run in a thread."""
187 token_name = get_token_name(app_name)
188- credentials = self.processor().validate_email(email, password,
189- email_token, token_name)
190+ credentials = self.processor.validate_email(email, password,
191+ email_token, token_name)
192 keyring_store_credentials(app_name, credentials)
193 return email
194 blocking(f, app_name, self.EmailValidated, self.EmailValidationError)
195@@ -234,7 +247,7 @@
196 """Call the matching method in the processor."""
197 def f():
198 """Inner function that will be run in a thread."""
199- return self.processor().request_password_reset_token(email)
200+ return self.processor.request_password_reset_token(email)
201 blocking(f, app_name, self.PasswordResetTokenSent,
202 self.PasswordResetError)
203
204@@ -257,8 +270,8 @@
205 """Call the matching method in the processor."""
206 def f():
207 """Inner function that will be run in a thread."""
208- return self.processor().set_new_password(email, token,
209- new_password)
210+ return self.processor.set_new_password(email, token,
211+ new_password)
212 blocking(f, app_name, self.PasswordChanged, self.PasswordChangeError)
213
214
215
216=== modified file 'ubuntu_sso/tests/test_account.py'
217--- ubuntu_sso/tests/test_account.py 2010-10-01 18:19:57 +0000
218+++ ubuntu_sso/tests/test_account.py 2010-11-01 20:39:44 +0000
219@@ -102,6 +102,9 @@
220 class FakedAccounts(object):
221 """Fake the accounts service."""
222
223+ def __init__(self):
224+ self.preferred_email = EMAIL
225+
226 def validate_email(self, email_token):
227 """Fake the email validation. Return a fix result."""
228 if email_token is None:
229@@ -114,6 +117,17 @@
230 else:
231 return STATUS_EMAIL_OK
232
233+ # pylint: disable=E0202, C0103
234+
235+ def me(self):
236+ """Fake the 'me' information."""
237+ return {u'username': u'Wh46bKY',
238+ u'preferred_email': self.preferred_email,
239+ u'displayname': u'',
240+ u'unverified_emails': [u'aaaaaa@example.com'],
241+ u'verified_emails': [],
242+ u'openid_identifier': u'Wh46bKY'}
243+
244
245 class FakedSSOServer(object):
246 """Fake an SSO server."""
247@@ -229,6 +243,29 @@
248 result = self.processor.login(**self.login_kwargs)
249 self.assertEqual(TOKEN, result, 'authentication was successful.')
250
251+ # is_validated
252+
253+ def test_is_validated(self):
254+ """If preferred email is not None, user is validated."""
255+ result = self.processor.is_validated(**self.login_kwargs)
256+ self.assertTrue(result, 'user must be validated.')
257+
258+ def test_is_not_validated(self):
259+ """If preferred email is None, user is not validated."""
260+ service = FakedSSOServer(None, None)
261+ service.accounts.preferred_email = None
262+ result = self.processor.is_validated(sso_service=service,
263+ **self.login_kwargs)
264+ self.assertFalse(result, 'user must not be validated.')
265+
266+ def test_is_not_validated_empty_result(self):
267+ """If preferred email is None, user is not validated."""
268+ service = FakedSSOServer(None, None)
269+ service.accounts.me = lambda: {}
270+ result = self.processor.is_validated(sso_service=service,
271+ **self.login_kwargs)
272+ self.assertFalse(result, 'user must not be validated.')
273+
274 # validate_email
275
276 def test_validate_email_if_status_ok(self):
277
278=== modified file 'ubuntu_sso/tests/test_gui.py'
279--- ubuntu_sso/tests/test_gui.py 2010-10-07 18:59:16 +0000
280+++ ubuntu_sso/tests/test_gui.py 2010-11-01 20:39:44 +0000
281@@ -866,6 +866,12 @@
282 self.ui.join_ok_button.clicked()
283 self.assertTrue(self._called)
284
285+ def test_user_and_pass_are_cached(self):
286+ """Username and password are temporarly cached for further use."""
287+ self.click_join_with_valid_data()
288+ self.assertEqual(self.ui.user_email, EMAIL)
289+ self.assertEqual(self.ui.user_password, PASSWORD)
290+
291
292 class NoTermsAndConditionsTestCase(UbuntuSSOClientTestCase):
293 """Test suite for the user registration (with no t&c link)."""
294@@ -1114,6 +1120,17 @@
295 dict(reply_handler=gui.NO_OP,
296 error_handler=gui.NO_OP)))
297
298+ def test_on_verify_token_button_clicked(self):
299+ """Verify token uses cached user_email and user_password."""
300+ self.ui.user_email = 'test@me.com'
301+ self.ui.user_password = 'yadda-yedda'
302+ self.ui.on_verify_token_button_clicked()
303+ self.assertEqual(self.ui.backend._called['validate_email'],
304+ ((APP_NAME, self.ui.user_email,
305+ self.ui.user_password, EMAIL_TOKEN),
306+ dict(reply_handler=gui.NO_OP,
307+ error_handler=gui.NO_OP)))
308+
309 def test_on_verify_token_button_shows_processing_page(self):
310 """Verify token button triggers call to backend."""
311 self.click_verify_email_with_valid_data()
312@@ -1431,6 +1448,12 @@
313 self.ui.on_login_error(app_name=APP_NAME, error=self.error)
314 self.assert_pages_visibility(login=True)
315
316+ def test_on_user_not_validated_morphs_to_verify_page(self):
317+ """On user not validated, the verify page is shown."""
318+ self.click_connect_with_valid_data()
319+ self.ui.on_user_not_validated(app_name=APP_NAME, email=EMAIL)
320+ self.assert_pages_visibility(verify_email=True)
321+
322 def test_on_login_error_a_warning_is_shown(self):
323 """On user login error, a warning is shown with proper wording."""
324 self.click_connect_with_valid_data()
325@@ -1468,6 +1491,12 @@
326 self.ui.login_ok_button.clicked()
327 self.assertTrue(self._called)
328
329+ def test_user_and_pass_are_cached(self):
330+ """Username and password are temporarly cached for further use."""
331+ self.click_connect_with_valid_data()
332+ self.assertEqual(self.ui.user_email, EMAIL)
333+ self.assertEqual(self.ui.user_password, PASSWORD)
334+
335
336 class LoginValidationTestCase(UbuntuSSOClientTestCase):
337 """Test suite for the user login validation."""
338@@ -1846,7 +1875,7 @@
339 """All the backend signals are listed to be binded."""
340 for sig in ('CaptchaGenerated', 'CaptchaGenerationError',
341 'UserRegistered', 'UserRegistrationError',
342- 'LoggedIn', 'LoginError',
343+ 'LoggedIn', 'LoginError', 'UserNotValidated',
344 'EmailValidated', 'EmailValidationError',
345 'PasswordResetTokenSent', 'PasswordResetError',
346 'PasswordChanged', 'PasswordChangeError'):
347@@ -1928,6 +1957,13 @@
348 self.ui._signals['LoginError'](mismatch_app_name, 'dummy')
349 self.assertFalse(self._called)
350
351+ def test_on_user_not_validated_is_not_called(self):
352+ """on_user_not_validated is not called if incorrect app_name."""
353+ self.patch(self.ui, 'on_user_not_validated', self._set_called)
354+ mismatch_app_name = self.ui.app_name * 2
355+ self.ui._signals['UserNotValidated'](mismatch_app_name, 'dummy')
356+ self.assertFalse(self._called)
357+
358 def test_on_password_reset_token_sent_is_not_called(self):
359 """on_password_reset_token_sent is not called if incorrect app_name."""
360 self.patch(self.ui, 'on_password_reset_token_sent', self._set_called)
361
362=== modified file 'ubuntu_sso/tests/test_main.py'
363--- ubuntu_sso/tests/test_main.py 2010-10-07 19:10:00 +0000
364+++ ubuntu_sso/tests/test_main.py 2010-11-01 20:39:44 +0000
365@@ -204,8 +204,11 @@
366 def test_login(self):
367 """Test that the login method works ok."""
368 d = Deferred()
369- self.create_mock_processor().login(EMAIL, PASSWORD, TOKEN_NAME)
370+ processor = self.create_mock_processor()
371+ processor.login(EMAIL, PASSWORD, TOKEN_NAME)
372 self.mocker.result(TOKEN)
373+ processor.is_validated(EMAIL, PASSWORD, TOKEN_NAME)
374+ self.mocker.result(True)
375 self.patch(ubuntu_sso.main, "blocking", fake_ok_blocking)
376 self.mocker.replay()
377
378@@ -220,13 +223,40 @@
379 sso_login_processor_class=self.mockprocessorclass)
380 self.patch(client, "LoggedIn", verify)
381 self.patch(client, "LoginError", d.errback)
382+ self.patch(client, "UserNotValidated", d.errback)
383+ client.login(APP_NAME, EMAIL, PASSWORD)
384+ return d
385+
386+ def test_login_user_not_validated(self):
387+ """Test that the login sends EmailNotValidated signal."""
388+ d = Deferred()
389+ processor = self.create_mock_processor()
390+ processor.login(EMAIL, PASSWORD, TOKEN_NAME)
391+ self.mocker.result(TOKEN)
392+ processor.is_validated(EMAIL, PASSWORD, TOKEN_NAME)
393+ self.mocker.result(False)
394+ self.patch(ubuntu_sso.main, "blocking", fake_ok_blocking)
395+ self.mocker.replay()
396+
397+ def verify(app_name, email):
398+ """The actual test."""
399+ self.assertEqual(app_name, APP_NAME)
400+ self.assertEqual(email, EMAIL)
401+ self.assertFalse(self.keyring_was_set, "Keyring should not be set")
402+ d.callback("Ok")
403+
404+ client = SSOLogin(self.mockbusname,
405+ sso_login_processor_class=self.mockprocessorclass)
406+ self.patch(client, "LoggedIn", d.errback)
407+ self.patch(client, "LoginError", d.errback)
408+ self.patch(client, "UserNotValidated", verify)
409 client.login(APP_NAME, EMAIL, PASSWORD)
410 return d
411
412 def test_login_error(self):
413 """Test that the login method fails as expected."""
414 d = Deferred()
415- self.mockprocessorclass = self.mocker.mock()
416+ self.create_mock_processor()
417 self.patch(ubuntu_sso.main, "blocking", fake_err_blocking)
418
419 def fake_gtn(*args):
420@@ -247,6 +277,7 @@
421 sso_login_processor_class=self.mockprocessorclass)
422 self.patch(client, "LoggedIn", d.errback)
423 self.patch(client, "LoginError", verify)
424+ self.patch(client, "UserNotValidated", d.errback)
425 client.login(APP_NAME, EMAIL, PASSWORD)
426 return d
427
428@@ -276,7 +307,7 @@
429 def test_validate_email_error(self):
430 """Test that the validate_email method fails as expected."""
431 d = Deferred()
432- self.mockprocessorclass = self.mocker.mock()
433+ self.create_mock_processor()
434 self.patch(ubuntu_sso.main, "blocking", fake_err_blocking)
435
436 def fake_gtn(*args):
437@@ -710,6 +741,10 @@
438
439 timeout = 5
440
441+ def setUp(self):
442+ super(CredentialsManagementFindClearTestCase, self).setUp()
443+ self.patch(ubuntu_sso.main, 'blocking', fake_ok_blocking)
444+
445 def test_find_credentials(self):
446 """The credentials are asked and returned in signals."""
447 self.create_mock_backend().find_credentials()

Subscribers

People subscribed via source and target branches