Merge lp:~ralsina/ubuntu-sso-client/validate-harder into lp:ubuntu-sso-client
- validate-harder
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Roberto Alsina |
Approved revision: | 764 |
Merged at revision: | 754 |
Proposed branch: | lp:~ralsina/ubuntu-sso-client/validate-harder |
Merge into: | lp:ubuntu-sso-client |
Diff against target: |
480 lines (+302/-12) 5 files modified
data/qt/current_user_sign_in.ui (+3/-0) data/qt/forgotten_password.ui (+3/-0) data/qt/reset_password.ui (+3/-0) ubuntu_sso/qt/controllers.py (+44/-4) ubuntu_sso/qt/tests/test_windows.py (+249/-8) |
To merge this branch: | bzr merge lp:~ralsina/ubuntu-sso-client/validate-harder |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Natalia Bidart (community) | Approve | ||
Review via email: mp+70320@code.launchpad.net |
Commit message
Implement enabling/disabling of the "submit" button according to the input data's validation in 3 pages:
* Current User Sign In
* Reset Password
* Confirm Email
Description of the change
Implement enabling/disabling of the "submit" button according to the input data's validation in 3 pages:
* Current User Sign In
* Reset Password
* Confirm Email
- 759. By Roberto Alsina
-
rename test classes
- 760. By Roberto Alsina
-
missing .
- 761. By Roberto Alsina
-
missing .s
Roberto Alsina (ralsina) wrote : | # |
> For the validation to be correct, I think this method:
>
> def is_correct_
> """Return if the password is correct."""
> return self.view.
>
> should be:
>
> def is_correct_
> """Return if the password is correct."""
> return unicode(
Those two are equivalent because QStrings and unicode strings comparison works.
Added it anyway to be explicit.
> Also, all docstrings need to end with a dot.
Fixed
> Question, shouldn't these checks in our code assert over a specific validation
> callback instead of any callable?
>
> self.view.
I agree, but it's the way it's done in all the other tests, and improving the mocker-based tests is
not exactly my forte ;-)
> Great work on all the tests added!
Thanks!
- 762. By Roberto Alsina
-
fixes
- 763. By Roberto Alsina
-
Lint fixes
Natalia Bidart (nataliabidart) wrote : | # |
There is a little conflict in
Text conflict in ubuntu_
1 conflicts encountered.
Natalia Bidart (nataliabidart) wrote : | # |
Other than that, the branch looks good!
- 764. By Roberto Alsina
-
resolve conflict
Preview Diff
1 | === modified file 'data/qt/current_user_sign_in.ui' |
2 | --- data/qt/current_user_sign_in.ui 2011-08-04 15:25:42 +0000 |
3 | +++ data/qt/current_user_sign_in.ui 2011-08-05 14:40:27 +0000 |
4 | @@ -122,6 +122,9 @@ |
5 | </item> |
6 | <item> |
7 | <widget class="QPushButton" name="sign_in_button"> |
8 | + <property name="enabled"> |
9 | + <bool>false</bool> |
10 | + </property> |
11 | <property name="text"> |
12 | <string>Sign In</string> |
13 | </property> |
14 | |
15 | === modified file 'data/qt/forgotten_password.ui' |
16 | --- data/qt/forgotten_password.ui 2011-08-04 15:25:42 +0000 |
17 | +++ data/qt/forgotten_password.ui 2011-08-05 14:40:27 +0000 |
18 | @@ -109,6 +109,9 @@ |
19 | </item> |
20 | <item> |
21 | <widget class="QPushButton" name="send_button"> |
22 | + <property name="enabled"> |
23 | + <bool>false</bool> |
24 | + </property> |
25 | <property name="text"> |
26 | <string/> |
27 | </property> |
28 | |
29 | === modified file 'data/qt/reset_password.ui' |
30 | --- data/qt/reset_password.ui 2011-08-04 15:25:42 +0000 |
31 | +++ data/qt/reset_password.ui 2011-08-05 14:40:27 +0000 |
32 | @@ -65,6 +65,9 @@ |
33 | </item> |
34 | <item> |
35 | <widget class="QPushButton" name="reset_password_button"> |
36 | + <property name="enabled"> |
37 | + <bool>false</bool> |
38 | + </property> |
39 | <property name="text"> |
40 | <string/> |
41 | </property> |
42 | |
43 | === modified file 'ubuntu_sso/qt/controllers.py' |
44 | --- ubuntu_sso/qt/controllers.py 2011-08-04 16:24:35 +0000 |
45 | +++ ubuntu_sso/qt/controllers.py 2011-08-05 14:40:27 +0000 |
46 | @@ -194,6 +194,17 @@ |
47 | self.backend.on_logged_in_cb = self.on_logged_in |
48 | self.view.ui.forgot_password_label.linkActivated.connect( |
49 | self.on_forgotten_password) |
50 | + self.view.ui.email_edit.textChanged.connect(self._validate) |
51 | + self.view.ui.password_edit.textChanged.connect(self._validate) |
52 | + |
53 | + def _validate(self): |
54 | + """Perform input validation.""" |
55 | + valid = True |
56 | + if not is_correct_email(unicode(self.view.ui.email_edit.text())): |
57 | + valid = False |
58 | + elif not unicode(self.view.ui.password_edit.text()): |
59 | + valid = False |
60 | + self.view.ui.sign_in_button.setEnabled(valid) |
61 | |
62 | def login(self): |
63 | """Perform the login using the self.backend.""" |
64 | @@ -465,12 +476,12 @@ |
65 | def is_correct_email_confirmation(self, email_address): |
66 | """Return that the email is the same.""" |
67 | logger.debug('SetUpAccountController.is_correct_email_confirmation') |
68 | - return self.view.ui.email_edit.text() == email_address |
69 | + return unicode(self.view.ui.email_edit.text()) == email_address |
70 | |
71 | def is_correct_password_confirmation(self, password): |
72 | """Return that the passwords are correct.""" |
73 | logger.debug('SetUpAccountController.is_correct_password_confirmation') |
74 | - return self.view.ui.password_edit.text() == password |
75 | + return unicode(self.view.ui.password_edit.text()) == password |
76 | |
77 | #pylint: disable=C0103 |
78 | @inlineCallbacks |
79 | @@ -621,8 +632,12 @@ |
80 | class ForgottenPasswordController(BackendController): |
81 | """Controller used to deal with the forgotten pwd page.""" |
82 | |
83 | - def __init__(self, title='', subtitle=''): |
84 | + def __init__(self, message_box=None, title='', subtitle=''): |
85 | """Create a new instance.""" |
86 | + super(ForgottenPasswordController, self).__init__() |
87 | + if message_box is None: |
88 | + message_box = QMessageBox |
89 | + self.message_box = message_box |
90 | super(ForgottenPasswordController, self).__init__(title, subtitle) |
91 | |
92 | def _register_fields(self): |
93 | @@ -647,6 +662,7 @@ |
94 | |
95 | def _connect_ui(self): |
96 | """Connect the diff signals from the Ui.""" |
97 | + self.view.email_address_line_edit.textChanged.connect(self._validate) |
98 | self.view.send_button.clicked.connect( |
99 | lambda: self.backend.request_password_reset_token( |
100 | self.view.wizard().app_name, |
101 | @@ -657,6 +673,11 @@ |
102 | self.on_password_reset_token_sent() |
103 | self.backend.on_password_reset_error_cb = self.on_password_reset_error |
104 | |
105 | + def _validate(self): |
106 | + """Validate that we have an email.""" |
107 | + email = unicode(self.view.email_address_line_edit.text()) |
108 | + self.view.send_button.setEnabled(is_correct_email(email)) |
109 | + |
110 | def on_try_again(self): |
111 | """Set back the widget to the initial state.""" |
112 | self.view.error_label.setVisible(False) |
113 | @@ -725,6 +746,25 @@ |
114 | self.backend.on_password_changed_cb = self.on_password_changed |
115 | self.backend.on_password_change_error_cb = \ |
116 | self.on_password_change_error |
117 | + self.view.ui.reset_code_line_edit.textChanged.connect(self._validate) |
118 | + self.view.ui.password_line_edit.textChanged.connect(self._validate) |
119 | + self.view.ui.confirm_password_line_edit.textChanged.connect( |
120 | + self._validate) |
121 | + |
122 | + def _validate(self): |
123 | + """Enable the submit button if data is valid.""" |
124 | + enabled = True |
125 | + code = unicode(self.view.ui.reset_code_line_edit.text()) |
126 | + password = unicode(self.view.ui.password_line_edit.text()) |
127 | + confirm_password = unicode( |
128 | + self.view.ui.confirm_password_line_edit.text()) |
129 | + if not is_min_required_password(password): |
130 | + enabled = False |
131 | + elif not self.is_correct_password_confirmation(confirm_password): |
132 | + enabled = False |
133 | + elif not code: |
134 | + enabled = False |
135 | + self.view.ui.reset_password_button.setEnabled(enabled) |
136 | |
137 | def _add_line_edits_validations(self): |
138 | """Add the validations to be use by the line edits.""" |
139 | @@ -756,7 +796,7 @@ |
140 | |
141 | def is_correct_password_confirmation(self, password): |
142 | """Return if the password is correct.""" |
143 | - return self.view.ui.password_line_edit.text() == password |
144 | + return unicode(self.view.ui.password_line_edit.text()) == password |
145 | |
146 | #pylint: disable=C0103 |
147 | @inlineCallbacks |
148 | |
149 | === modified file 'ubuntu_sso/qt/tests/test_windows.py' |
150 | --- ubuntu_sso/qt/tests/test_windows.py 2011-08-04 15:25:42 +0000 |
151 | +++ ubuntu_sso/qt/tests/test_windows.py 2011-08-05 14:40:27 +0000 |
152 | @@ -70,7 +70,7 @@ |
153 | is_correct_email) |
154 | |
155 | #ignore the comon mocker issues with lint |
156 | -# pylint: disable=W0212,W0104,W0106 |
157 | +# pylint: disable=W0212,W0104,W0106,E1103 |
158 | |
159 | |
160 | class ChooseSignInControllerTestCase(MockerTestCase): |
161 | @@ -157,6 +157,8 @@ |
162 | self.backend.on_logged_in_cb = MATCH(callable) |
163 | self.view.ui.forgot_password_label.linkActivated.connect( |
164 | MATCH(callable)) |
165 | + self.view.ui.email_edit.textChanged.connect(MATCH(callable)) |
166 | + self.view.ui.password_edit.textChanged.connect(MATCH(callable)) |
167 | self.mocker.replay() |
168 | self.controller._connect_ui() |
169 | |
170 | @@ -167,6 +169,94 @@ |
171 | self.assertEqual(self.controller._subtitle, 'the subtitle') |
172 | |
173 | |
174 | +class FakeLineEdit(object): |
175 | + |
176 | + """A fake QLineEdit.""" |
177 | + |
178 | + def __init__(self, *args): |
179 | + """Initialize.""" |
180 | + self._text = u"" |
181 | + |
182 | + def text(self): |
183 | + """Save text.""" |
184 | + return self._text |
185 | + |
186 | + # setText is inherited |
187 | + # pylint: disable=C0103 |
188 | + def setText(self, text): |
189 | + """Return saved text.""" |
190 | + self._text = text |
191 | + |
192 | + |
193 | +class FakeCurrentUserPage(object): |
194 | + |
195 | + """Fake CurrentuserPage.""" |
196 | + |
197 | + def __init__(self, *args): |
198 | + """Initialize.""" |
199 | + self.ui = self |
200 | + self.ui.email_edit = FakeLineEdit() |
201 | + self.ui.password_edit = FakeLineEdit() |
202 | + self.sign_in_button = self |
203 | + self._enabled = False |
204 | + |
205 | + # setEnabled is inherited |
206 | + # pylint: disable=C0103 |
207 | + def setEnabled(self, value): |
208 | + """Fake setEnabled.""" |
209 | + self._enabled = value |
210 | + |
211 | + def enabled(self): |
212 | + """Fake enabled.""" |
213 | + return self._enabled |
214 | + |
215 | + |
216 | +class CurrentUserControllerValidationTest(TestCase): |
217 | + |
218 | + """Tests for CurrentUserController, but without Mocker.""" |
219 | + |
220 | + def setUp(self): |
221 | + """Setup test.""" |
222 | + super(CurrentUserControllerValidationTest, self).setUp() |
223 | + self.message_box = FakeMessageBox() |
224 | + self.controller = CurrentUserController( |
225 | + message_box=self.message_box) |
226 | + self.controller.view = FakeCurrentUserPage() |
227 | + self._called = False |
228 | + |
229 | + def _set_called(self, *args, **kwargs): |
230 | + """Store 'args' and 'kwargs' for test assertions.""" |
231 | + self._called = (args, kwargs) |
232 | + |
233 | + def test_valid(self): |
234 | + """Enable the button with a valid email/password.""" |
235 | + self.controller.view.email_edit.setText("a@b") |
236 | + self.controller.view.password_edit.setText("pass") |
237 | + self.controller._validate() |
238 | + self.assertTrue(self.controller.view.sign_in_button.enabled()) |
239 | + |
240 | + def test_invalid_email(self): |
241 | + """The submit button should be disabled with an invalid email.""" |
242 | + self.controller.view.email_edit.setText("ab") |
243 | + self.controller.view.password_edit.setText("pass") |
244 | + self.controller._validate() |
245 | + self.assertFalse(self.controller.view.sign_in_button.enabled()) |
246 | + |
247 | + def test_invalid_password(self): |
248 | + """The submit button should be disabled with an invalid password.""" |
249 | + self.controller.view.email_edit.setText("a@b") |
250 | + self.controller.view.password_edit.setText("") |
251 | + self.controller._validate() |
252 | + self.assertFalse(self.controller.view.sign_in_button.enabled()) |
253 | + |
254 | + def test_invalid_both(self): |
255 | + """The submit button should be disabled with invalid data.""" |
256 | + self.controller.view.email_edit.setText("ab") |
257 | + self.controller.view.password_edit.setText("") |
258 | + self.controller._validate() |
259 | + self.assertFalse(self.controller.view.sign_in_button.enabled()) |
260 | + |
261 | + |
262 | class SetUpAccountControllerTestCase(MockerTestCase): |
263 | """test the controller used to setup a new account.""" |
264 | |
265 | @@ -446,7 +536,7 @@ |
266 | |
267 | class FakeView(object): |
268 | |
269 | - """A fake view""" |
270 | + """A fake view.""" |
271 | |
272 | app_name = "TestApp" |
273 | |
274 | @@ -455,12 +545,13 @@ |
275 | return self |
276 | |
277 | |
278 | -class SetupAccountControllerTest2(TestCase): |
279 | +class SetupAccountControllerValidationTest(TestCase): |
280 | |
281 | - """Tests for SetupAccountController, but without Mocker""" |
282 | + """Tests for SetupAccountController, but without Mocker.""" |
283 | |
284 | def setUp(self): |
285 | - super(SetupAccountControllerTest2, self).setUp() |
286 | + """Set the different tests.""" |
287 | + super(SetupAccountControllerValidationTest, self).setUp() |
288 | self.message_box = FakeMessageBox() |
289 | self.controller = SetUpAccountController(message_box=self.message_box) |
290 | self.patch(self.controller, '_refresh_captcha', self._set_called) |
291 | @@ -591,11 +682,12 @@ |
292 | self.controller.validate_email() |
293 | |
294 | |
295 | -class EmailVerificationControllerTestCase2(TestCase): |
296 | - """Tests for EmailVerificationController, but without Mocker""" |
297 | +class EmailVerificationControllerValidationTestCase(TestCase): |
298 | + """Tests for EmailVerificationController, but without Mocker.""" |
299 | |
300 | def setUp(self): |
301 | - super(EmailVerificationControllerTestCase2, self).setUp() |
302 | + """Set the different tests.""" |
303 | + super(EmailVerificationControllerValidationTestCase, self).setUp() |
304 | self.message_box = FakeMessageBox() |
305 | self.controller = EmailVerificationController( |
306 | message_box=self.message_box) |
307 | @@ -768,6 +860,75 @@ |
308 | self.controller.setupUi(self.view) |
309 | |
310 | |
311 | +class FakeForgottenPasswordPage(FakeView): |
312 | + """Fake the page.""" |
313 | + |
314 | + def __init__(self): |
315 | + self.email_address_line_edit = self |
316 | + self.send_button = self |
317 | + self._text = "" |
318 | + self._enabled = False |
319 | + super(FakeForgottenPasswordPage, self).__init__() |
320 | + |
321 | + def text(self): |
322 | + """Return text.""" |
323 | + return self._text |
324 | + |
325 | + # setText, setEnabled are inherited |
326 | + # pylint: disable=C0103 |
327 | + def setText(self, text): |
328 | + """Save text.""" |
329 | + self._text = text |
330 | + |
331 | + def setEnabled(self, value): |
332 | + """Fake setEnabled.""" |
333 | + self._enabled = value |
334 | + |
335 | + def enabled(self): |
336 | + """Fake enabled.""" |
337 | + return self._enabled |
338 | + |
339 | + |
340 | +class ForgottenPasswordControllerValidationTest(TestCase): |
341 | + |
342 | + """Tests for ForgottenPasswordController, but without Mocker.""" |
343 | + |
344 | + def setUp(self): |
345 | + """Set the different tests.""" |
346 | + super(ForgottenPasswordControllerValidationTest, self).setUp() |
347 | + self.message_box = FakeMessageBox() |
348 | + self.controller = ForgottenPasswordController( |
349 | + message_box=self.message_box) |
350 | + self.controller.view = FakeForgottenPasswordPage() |
351 | + self._called = False |
352 | + |
353 | + def _set_called(self, *args, **kwargs): |
354 | + """Store 'args' and 'kwargs' for test assertions.""" |
355 | + self._called = (args, kwargs) |
356 | + |
357 | + def test_valid(self): |
358 | + """The submit button should be enabled with a valid email.""" |
359 | + self.controller.view.email_address_line_edit.setText("a@b") |
360 | + self.assertNotEqual(unicode( |
361 | + self.controller.view.email_address_line_edit.text()), u"") |
362 | + self.controller._validate() |
363 | + self.assertTrue(self.controller.view.send_button.enabled()) |
364 | + |
365 | + def test_invalid(self): |
366 | + """The submit button should be disabled with an invalid email.""" |
367 | + self.controller.view.email_address_line_edit.setText("ab") |
368 | + self.assertNotEqual( |
369 | + unicode(self.controller.view.email_address_line_edit.text()), u"") |
370 | + self.controller._validate() |
371 | + self.assertFalse(self.controller.view.send_button.enabled()) |
372 | + |
373 | + def test_empty(self): |
374 | + """The submit button should be disabled without email.""" |
375 | + self.assertEqual( |
376 | + unicode(self.controller.view.email_address_line_edit.text()), u"") |
377 | + self.assertFalse(self.controller.view.send_button.enabled()) |
378 | + |
379 | + |
380 | class ForgottenPasswordControllerTestCase(MockerTestCase): |
381 | """Test the controller of the fogotten password page.""" |
382 | |
383 | @@ -814,6 +975,7 @@ |
384 | |
385 | def test_connect_ui(self): |
386 | """Test that the correct ui signals are connected.""" |
387 | + self.view.email_address_line_edit.textChanged.connect(MATCH(callable)) |
388 | self.view.send_button.clicked.connect(MATCH(callable)) |
389 | self.view.try_again_button.clicked.connect( |
390 | self.controller.on_try_again) |
391 | @@ -892,6 +1054,13 @@ |
392 | self.controller.on_password_changed |
393 | self.backend.on_password_change_error_cb = \ |
394 | self.controller.on_password_change_error |
395 | + self.view.ui.reset_code_line_edit.textChanged.connect( |
396 | + MATCH(callable)) |
397 | + self.view.ui.password_line_edit.textChanged.connect( |
398 | + MATCH(callable)) |
399 | + self.view.ui.confirm_password_line_edit.textChanged.connect( |
400 | + MATCH(callable)) |
401 | + |
402 | self.mocker.replay() |
403 | self.controller._connect_ui() |
404 | |
405 | @@ -947,3 +1116,75 @@ |
406 | self.mocker.replay() |
407 | self.assertFalse(self.controller.is_correct_password_confirmation( |
408 | password)) |
409 | + |
410 | + |
411 | +class FakeResetPasswordPage(object): |
412 | + |
413 | + """Fake ResetPasswordPage.""" |
414 | + |
415 | + def __init__(self, *args): |
416 | + """Initialize.""" |
417 | + self.ui = self |
418 | + self.ui.reset_code_line_edit = FakeLineEdit() |
419 | + self.ui.password_line_edit = FakeLineEdit() |
420 | + self.ui.confirm_password_line_edit = FakeLineEdit() |
421 | + self.reset_password_button = self |
422 | + self._enabled = 9 # Intentionally wrong. |
423 | + |
424 | + # setEnabled is inherited |
425 | + # pylint: disable=C0103 |
426 | + def setEnabled(self, value): |
427 | + """Fake setEnabled.""" |
428 | + self._enabled = value |
429 | + |
430 | + def enabled(self): |
431 | + """Fake enabled.""" |
432 | + return self._enabled |
433 | + |
434 | + |
435 | +class ResetPasswordControllerValidationTest(TestCase): |
436 | + |
437 | + """Tests for ResetPasswordController, but without Mocker.""" |
438 | + |
439 | + def setUp(self): |
440 | + """Setup test.""" |
441 | + super(ResetPasswordControllerValidationTest, self).setUp() |
442 | + self.controller = ResetPasswordController() |
443 | + self.controller.view = FakeResetPasswordPage() |
444 | + self._called = False |
445 | + |
446 | + def _set_called(self, *args, **kwargs): |
447 | + """Store 'args' and 'kwargs' for test assertions.""" |
448 | + self._called = (args, kwargs) |
449 | + |
450 | + def test_valid(self): |
451 | + """Enable the button with valid data.""" |
452 | + self.controller.view.reset_code_line_edit.setText("ABCD") |
453 | + self.controller.view.password_line_edit.setText("1234567A") |
454 | + self.controller.view.confirm_password_line_edit.setText("1234567A") |
455 | + self.controller._validate() |
456 | + self.assertTrue(self.controller.view.reset_password_button.enabled()) |
457 | + |
458 | + def test_invalid_code(self): |
459 | + """Disable the button with an invalid code.""" |
460 | + self.controller.view.reset_code_line_edit.setText("") |
461 | + self.controller.view.password_line_edit.setText("1234567A") |
462 | + self.controller.view.confirm_password_line_edit.setText("1234567A") |
463 | + self.controller._validate() |
464 | + self.assertFalse(self.controller.view.reset_password_button.enabled()) |
465 | + |
466 | + def test_invalid_password(self): |
467 | + """Disable the button with an invalid password.""" |
468 | + self.controller.view.reset_code_line_edit.setText("") |
469 | + self.controller.view.password_line_edit.setText("1234567") |
470 | + self.controller.view.confirm_password_line_edit.setText("1234567") |
471 | + self.controller._validate() |
472 | + self.assertFalse(self.controller.view.reset_password_button.enabled()) |
473 | + |
474 | + def test_invalid_confirm(self): |
475 | + """Disable the button with an invalid password confirm.""" |
476 | + self.controller.view.reset_code_line_edit.setText("") |
477 | + self.controller.view.password_line_edit.setText("1234567A") |
478 | + self.controller.view.confirm_password_line_edit.setText("1234567") |
479 | + self.controller._validate() |
480 | + self.assertFalse(self.controller.view.reset_password_button.enabled()) |
For the validation to be correct, I think this method:
def is_correct_ password_ confirmation( self, password): ui.password_ line_edit. text() == password
"""Return if the password is correct."""
return self.view.
should be:
def is_correct_ password_ confirmation( self, password): self.view. ui.password_ line_edit. text()) == password
"""Return if the password is correct."""
return unicode(
Also, all docstrings need to end with a dot.
Question, shouldn't these checks in our code assert over a specific validation callback instead of any callable?
self.view. ui.password_ edit.textChange d.connect( MATCH(callable) )
Great work on all the tests added!