Merge lp:~ralsina/ubuntu-sso-client/validate-harder into lp:ubuntu-sso-client

Proposed by Roberto Alsina
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
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

To post a comment you must log in.
759. By Roberto Alsina

rename test classes

760. By Roberto Alsina

missing .

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

For the validation to be correct, I think this method:

    def is_correct_password_confirmation(self, password):
        """Return if the password is correct."""
        return self.view.ui.password_line_edit.text() == password

should be:

    def is_correct_password_confirmation(self, password):
        """Return if the password is correct."""
        return unicode(self.view.ui.password_line_edit.text()) == password

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.textChanged.connect(MATCH(callable))

Great work on all the tests added!

review: Needs Fixing
761. By Roberto Alsina

missing .s

Revision history for this message
Roberto Alsina (ralsina) wrote :

> For the validation to be correct, I think this method:
>
> def is_correct_password_confirmation(self, password):
> """Return if the password is correct."""
> return self.view.ui.password_line_edit.text() == password
>
> should be:
>
> def is_correct_password_confirmation(self, password):
> """Return if the password is correct."""
> return unicode(self.view.ui.password_line_edit.text()) == password

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.ui.password_edit.textChanged.connect(MATCH(callable))

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

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

There is a little conflict in

Text conflict in ubuntu_sso/qt/controllers.py
1 conflicts encountered.

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Other than that, the branch looks good!

review: Approve
764. By Roberto Alsina

resolve conflict

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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())

Subscribers

People subscribed via source and target branches