Merge lp:~matiasb/canonical-identity-provider/models-validation into lp:canonical-identity-provider/release
- models-validation
- Merge into trunk
Proposed by
Matias Bordese
Status: | Merged |
---|---|
Approved by: | Natalia Bidart |
Approved revision: | no longer in the source branch. |
Merged at revision: | 556 |
Proposed branch: | lp:~matiasb/canonical-identity-provider/models-validation |
Merge into: | lp:canonical-identity-provider/release |
Diff against target: |
363 lines (+103/-71) 9 files modified
api/tests/test_handlers.py (+15/-2) api/v10/forms.py (+3/-19) api/v10/handlers.py (+12/-16) identityprovider/forms.py (+5/-23) identityprovider/models/account.py (+18/-0) identityprovider/models/emailaddress.py (+2/-1) identityprovider/tests/unit/test_validators.py (+26/-0) identityprovider/validators.py (+21/-0) webui/views/ui.py (+1/-10) |
To merge this branch: | bzr merge lp:~matiasb/canonical-identity-provider/models-validation |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Natalia Bidart (community) | Approve | ||
Review via email: mp+139547@code.launchpad.net |
Commit message
Refactored password and email validation when creating an account using (reusable) validators.
Description of the change
Refactored password and email validation when creating an account using (reusable) validators.
Updated forms/views requiring password validation.
Kept flexibility at the model level (by using set_password or create_account).
To post a comment you must log in.
Revision history for this message
Matias Bordese (matiasb) wrote : | # |
> Code looks good. This piece seems buggy to me, perhaps you can fix it and add
> a test for it as well?
>
> 108 - 'errors': [PASSWORD_
> 109 + 'errors': e.message
Added test, using .messages returns a list (of possible multiple errors).
> Also, could you please add a couple of more tests to
> PasswordPolicyV
Added.
Revision history for this message
Natalia Bidart (nataliabidart) wrote : | # |
Looks good, thanks.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'api/tests/test_handlers.py' | |||
2 | --- api/tests/test_handlers.py 2012-12-04 13:44:46 +0000 | |||
3 | +++ api/tests/test_handlers.py 2012-12-13 20:21:20 +0000 | |||
4 | @@ -27,6 +27,7 @@ | |||
5 | 27 | EmailStatus, | 27 | EmailStatus, |
6 | 28 | LoginTokenType, | 28 | LoginTokenType, |
7 | 29 | ) | 29 | ) |
8 | 30 | from identityprovider.validators import PASSWORD_POLICY_ERROR | ||
9 | 30 | 31 | ||
10 | 31 | from api.tests.utils import ( | 32 | from api.tests.utils import ( |
11 | 32 | AnonAPITestCase, | 33 | AnonAPITestCase, |
12 | @@ -51,8 +52,20 @@ | |||
13 | 51 | response = self.api.registrations.set_new_password( | 52 | response = self.api.registrations.set_new_password( |
14 | 52 | email='test@example.com', token=authtoken.token, | 53 | email='test@example.com', token=authtoken.token, |
15 | 53 | new_password='pass') | 54 | new_password='pass') |
18 | 54 | 55 | self.assertEqual(response['status'], "error") | |
19 | 55 | self.assertEqual(response['status'], "error") | 56 | |
20 | 57 | def test_set_new_password_invalid(self): | ||
21 | 58 | email_address = self.factory.make_email_address() | ||
22 | 59 | account = self.factory.make_account(email=email_address) | ||
23 | 60 | authtoken = AuthTokenFactory().new(account, email_address, | ||
24 | 61 | email_address, | ||
25 | 62 | LoginTokenType.PASSWORDRECOVERY, | ||
26 | 63 | None) | ||
27 | 64 | |||
28 | 65 | response = self.api.registrations.set_new_password( | ||
29 | 66 | email=email_address, token=authtoken.token, new_password='pass') | ||
30 | 67 | self.assertEqual(response['status'], "error") | ||
31 | 68 | self.assertEqual(response['errors'], [PASSWORD_POLICY_ERROR]) | ||
32 | 56 | 69 | ||
33 | 57 | def test_reset_token_when_email_is_invalid(self): | 70 | def test_reset_token_when_email_is_invalid(self): |
34 | 58 | try: | 71 | try: |
35 | 59 | 72 | ||
36 | === modified file 'api/v10/forms.py' | |||
37 | --- api/v10/forms.py 2012-12-03 16:51:35 +0000 | |||
38 | +++ api/v10/forms.py 2012-12-13 20:21:20 +0000 | |||
39 | @@ -7,18 +7,14 @@ | |||
40 | 7 | from django.forms import fields | 7 | from django.forms import fields |
41 | 8 | from django.utils.translation import ugettext as _ | 8 | from django.utils.translation import ugettext as _ |
42 | 9 | 9 | ||
43 | 10 | from identityprovider.utils import password_policy_compliant | ||
44 | 11 | from identityprovider.models.captcha import Captcha | 10 | from identityprovider.models.captcha import Captcha |
50 | 12 | 11 | from identityprovider.validators import validate_password_policy | |
46 | 13 | |||
47 | 14 | PASSWORD_POLICY_ERROR = _("Password must be at least " | ||
48 | 15 | "8 characters long, and must contain at least one " | ||
49 | 16 | "number and an upper case letter.") | ||
51 | 17 | 12 | ||
52 | 18 | 13 | ||
53 | 19 | class WebserviceCreateAccountForm(forms.Form): | 14 | class WebserviceCreateAccountForm(forms.Form): |
54 | 20 | email = fields.EmailField() | 15 | email = fields.EmailField() |
56 | 21 | password = fields.CharField(max_length=256) | 16 | password = fields.CharField(max_length=256, |
57 | 17 | validators=[validate_password_policy]) | ||
58 | 22 | captcha_id = fields.CharField(max_length=1024) | 18 | captcha_id = fields.CharField(max_length=1024) |
59 | 23 | captcha_solution = fields.CharField(max_length=256) | 19 | captcha_solution = fields.CharField(max_length=256) |
60 | 24 | remote_ip = fields.CharField(max_length=256) | 20 | remote_ip = fields.CharField(max_length=256) |
61 | @@ -28,18 +24,6 @@ | |||
62 | 28 | empty_value='desktop', required=False) | 24 | empty_value='desktop', required=False) |
63 | 29 | validate_redirect_to = fields.CharField(required=False) | 25 | validate_redirect_to = fields.CharField(required=False) |
64 | 30 | 26 | ||
65 | 31 | def clean_password(self): | ||
66 | 32 | if 'password' in self.cleaned_data: | ||
67 | 33 | password = self.cleaned_data['password'] | ||
68 | 34 | try: | ||
69 | 35 | str(password) | ||
70 | 36 | except UnicodeEncodeError: | ||
71 | 37 | raise forms.ValidationError( | ||
72 | 38 | _("Invalid characters in password")) | ||
73 | 39 | if not password_policy_compliant(password): | ||
74 | 40 | raise forms.ValidationError(PASSWORD_POLICY_ERROR) | ||
75 | 41 | return password | ||
76 | 42 | |||
77 | 43 | def clean_validate_redirect_to(self): | 27 | def clean_validate_redirect_to(self): |
78 | 44 | validate_redirect_to = self.cleaned_data.get('validate_redirect_to') | 28 | validate_redirect_to = self.cleaned_data.get('validate_redirect_to') |
79 | 45 | # return None instead of '' as the default value | 29 | # return None instead of '' as the default value |
80 | 46 | 30 | ||
81 | === modified file 'api/v10/handlers.py' | |||
82 | --- api/v10/handlers.py 2012-12-04 18:51:42 +0000 | |||
83 | +++ api/v10/handlers.py 2012-12-13 20:21:20 +0000 | |||
84 | @@ -4,6 +4,7 @@ | |||
85 | 4 | from datetime import datetime, timedelta | 4 | from datetime import datetime, timedelta |
86 | 5 | 5 | ||
87 | 6 | from django.conf import settings | 6 | from django.conf import settings |
88 | 7 | from django.core.exceptions import ValidationError | ||
89 | 7 | from django.core.serializers.json import DateTimeAwareJSONEncoder | 8 | from django.core.serializers.json import DateTimeAwareJSONEncoder |
90 | 8 | from django.http import ( | 9 | from django.http import ( |
91 | 9 | HttpResponseBadRequest, | 10 | HttpResponseBadRequest, |
92 | @@ -36,10 +37,8 @@ | |||
93 | 36 | application_token_created, | 37 | application_token_created, |
94 | 37 | application_token_invalidated, | 38 | application_token_invalidated, |
95 | 38 | ) | 39 | ) |
100 | 39 | from identityprovider.utils import ( | 40 | from identityprovider.validators import validate_password_policy |
101 | 40 | encrypt_launchpad_password, | 41 | |
98 | 41 | password_policy_compliant, | ||
99 | 42 | ) | ||
102 | 43 | from oauth_backend.models import Token | 42 | from oauth_backend.models import Token |
103 | 44 | 43 | ||
104 | 45 | from api.v10.decorators import ( | 44 | from api.v10.decorators import ( |
105 | @@ -48,7 +47,6 @@ | |||
106 | 48 | named_operation, | 47 | named_operation, |
107 | 49 | ) | 48 | ) |
108 | 50 | from api.v10.forms import ( | 49 | from api.v10.forms import ( |
109 | 51 | PASSWORD_POLICY_ERROR, | ||
110 | 52 | WebserviceCreateAccountForm, | 50 | WebserviceCreateAccountForm, |
111 | 53 | ) | 51 | ) |
112 | 54 | 52 | ||
113 | @@ -199,15 +197,13 @@ | |||
114 | 199 | 197 | ||
115 | 200 | platform = cleaned_data['platform'] | 198 | platform = cleaned_data['platform'] |
116 | 201 | redirection_url = cleaned_data['validate_redirect_to'] | 199 | redirection_url = cleaned_data['validate_redirect_to'] |
119 | 202 | encrypted_password = encrypt_launchpad_password( | 200 | password = cleaned_data['password'] |
118 | 203 | cleaned_data['password']) | ||
120 | 204 | displayname = cleaned_data['displayname'] | 201 | displayname = cleaned_data['displayname'] |
121 | 205 | email = cleaned_data['email'] | 202 | email = cleaned_data['email'] |
122 | 206 | if platform in ['desktop', 'mobile']: | 203 | if platform in ['desktop', 'mobile']: |
123 | 207 | account = Account.objects.create_account( | 204 | account = Account.objects.create_account( |
127 | 208 | displayname, email, | 205 | displayname, email, password, email_validated=False) |
128 | 209 | encrypted_password, password_encrypted=True, | 206 | encrypted_password = account.encrypted_password |
126 | 210 | email_validated=False) | ||
129 | 211 | 207 | ||
130 | 212 | if platform == 'desktop': | 208 | if platform == 'desktop': |
131 | 213 | token = AuthTokenFactory().new_api_email_validation_token( | 209 | token = AuthTokenFactory().new_api_email_validation_token( |
132 | @@ -275,15 +271,15 @@ | |||
133 | 275 | 'status': 'error', | 271 | 'status': 'error', |
134 | 276 | 'message': "Wrong token, request new one." | 272 | 'message': "Wrong token, request new one." |
135 | 277 | } | 273 | } |
137 | 278 | if not password_policy_compliant(new_password): | 274 | |
138 | 275 | try: | ||
139 | 276 | validate_password_policy(new_password) | ||
140 | 277 | except ValidationError, e: | ||
141 | 279 | return { | 278 | return { |
142 | 280 | 'status': 'error', | 279 | 'status': 'error', |
144 | 281 | 'errors': [PASSWORD_POLICY_ERROR] | 280 | 'errors': e.messages, |
145 | 282 | } | 281 | } |
150 | 283 | password_obj = token.requester.accountpassword | 282 | token.requester.set_password(new_password) |
147 | 284 | password_obj.password = encrypt_launchpad_password(new_password) | ||
148 | 285 | password_obj.save() | ||
149 | 286 | |||
151 | 287 | token.consume() | 283 | token.consume() |
152 | 288 | 284 | ||
153 | 289 | return { | 285 | return { |
154 | 290 | 286 | ||
155 | === modified file 'identityprovider/forms.py' | |||
156 | --- identityprovider/forms.py 2012-10-30 12:15:29 +0000 | |||
157 | +++ identityprovider/forms.py 2012-12-13 20:21:20 +0000 | |||
158 | @@ -21,7 +21,9 @@ | |||
159 | 21 | from identityprovider.models.const import EmailStatus | 21 | from identityprovider.models.const import EmailStatus |
160 | 22 | from identityprovider.utils import ( | 22 | from identityprovider.utils import ( |
161 | 23 | encrypt_launchpad_password, | 23 | encrypt_launchpad_password, |
163 | 24 | password_policy_compliant, | 24 | ) |
164 | 25 | from identityprovider.validators import ( | ||
165 | 26 | validate_password_policy, | ||
166 | 25 | ) | 27 | ) |
167 | 26 | from identityprovider.fields import OATHPasswordField | 28 | from identityprovider.fields import OATHPasswordField |
168 | 27 | from identityprovider.widgets import ( | 29 | from identityprovider.widgets import ( |
169 | @@ -29,8 +31,6 @@ | |||
170 | 29 | ROAwareSelect, | 31 | ROAwareSelect, |
171 | 30 | ) | 32 | ) |
172 | 31 | 33 | ||
173 | 32 | PASSWORD_ERROR = _("Password must be at least 8 characters long, and must " | ||
174 | 33 | "contain at least one number and an upper case letter.") | ||
175 | 34 | 34 | ||
176 | 35 | logger = logging.getLogger('sso') | 35 | logger = logging.getLogger('sso') |
177 | 36 | 36 | ||
178 | @@ -81,6 +81,7 @@ | |||
179 | 81 | class ResetPasswordForm(forms.Form): | 81 | class ResetPasswordForm(forms.Form): |
180 | 82 | password = fields.CharField( | 82 | password = fields.CharField( |
181 | 83 | error_messages=default_errors, | 83 | error_messages=default_errors, |
182 | 84 | validators=[validate_password_policy], | ||
183 | 84 | widget=widgets.PasswordInput(attrs={ | 85 | widget=widgets.PasswordInput(attrs={ |
184 | 85 | 'class': 'textType', | 86 | 'class': 'textType', |
185 | 86 | 'size': '20', | 87 | 'size': '20', |
186 | @@ -94,18 +95,6 @@ | |||
187 | 94 | }), | 95 | }), |
188 | 95 | ) | 96 | ) |
189 | 96 | 97 | ||
190 | 97 | def clean_password(self): | ||
191 | 98 | if 'password' in self.cleaned_data: | ||
192 | 99 | password = self.cleaned_data['password'] | ||
193 | 100 | try: | ||
194 | 101 | str(password) | ||
195 | 102 | except UnicodeEncodeError: | ||
196 | 103 | raise forms.ValidationError( | ||
197 | 104 | _("Invalid characters in password")) | ||
198 | 105 | if not password_policy_compliant(password): | ||
199 | 106 | raise forms.ValidationError(PASSWORD_ERROR) | ||
200 | 107 | return password | ||
201 | 108 | |||
202 | 109 | def clean(self): | 98 | def clean(self): |
203 | 110 | cleaned_data = self.cleaned_data | 99 | cleaned_data = self.cleaned_data |
204 | 111 | password = cleaned_data.get('password') | 100 | password = cleaned_data.get('password') |
205 | @@ -171,6 +160,7 @@ | |||
206 | 171 | ) | 160 | ) |
207 | 172 | password = fields.CharField( | 161 | password = fields.CharField( |
208 | 173 | required=False, | 162 | required=False, |
209 | 163 | validators=[validate_password_policy], | ||
210 | 174 | widget=widgets.PasswordInput(attrs={ | 164 | widget=widgets.PasswordInput(attrs={ |
211 | 175 | 'class': 'disableAutoComplete textType', | 165 | 'class': 'disableAutoComplete textType', |
212 | 176 | 'size': '20', | 166 | 'size': '20', |
213 | @@ -237,14 +227,6 @@ | |||
214 | 237 | (self.data.get('currentpassword') or | 227 | (self.data.get('currentpassword') or |
215 | 238 | self.data.get('passwordconfirm'))): | 228 | self.data.get('passwordconfirm'))): |
216 | 239 | raise forms.ValidationError(_("Required field")) | 229 | raise forms.ValidationError(_("Required field")) |
217 | 240 | if password: | ||
218 | 241 | try: | ||
219 | 242 | str(password) | ||
220 | 243 | except UnicodeEncodeError: | ||
221 | 244 | raise forms.ValidationError( | ||
222 | 245 | _("Invalid characters in password")) | ||
223 | 246 | if not password_policy_compliant(password): | ||
224 | 247 | raise forms.ValidationError(PASSWORD_ERROR) | ||
225 | 248 | return password | 230 | return password |
226 | 249 | 231 | ||
227 | 250 | def clean_passwordconfirm(self): | 232 | def clean_passwordconfirm(self): |
228 | 251 | 233 | ||
229 | === modified file 'identityprovider/models/account.py' | |||
230 | --- identityprovider/models/account.py 2012-10-29 12:47:16 +0000 | |||
231 | +++ identityprovider/models/account.py 2012-12-13 20:21:20 +0000 | |||
232 | @@ -278,6 +278,24 @@ | |||
233 | 278 | sname = self.displayname.split(' ') | 278 | sname = self.displayname.split(' ') |
234 | 279 | return sname[0] | 279 | return sname[0] |
235 | 280 | 280 | ||
236 | 281 | @property | ||
237 | 282 | def encrypted_password(self): | ||
238 | 283 | try: | ||
239 | 284 | accountpassword = self.accountpassword | ||
240 | 285 | password = accountpassword.password | ||
241 | 286 | except AccountPassword.DoesNotExist: | ||
242 | 287 | password = None | ||
243 | 288 | return password | ||
244 | 289 | |||
245 | 290 | def set_password(self, password, salt=None): | ||
246 | 291 | try: | ||
247 | 292 | accountpassword = self.accountpassword | ||
248 | 293 | except AccountPassword.DoesNotExist: | ||
249 | 294 | accountpassword = AccountPassword(account=self, password='invalid') | ||
250 | 295 | accountpassword.password = encrypt_launchpad_password( | ||
251 | 296 | password, salt=salt) | ||
252 | 297 | accountpassword.save() | ||
253 | 298 | |||
254 | 281 | def get_and_delete_messages(self): | 299 | def get_and_delete_messages(self): |
255 | 282 | return [] | 300 | return [] |
256 | 283 | 301 | ||
257 | 284 | 302 | ||
258 | === modified file 'identityprovider/models/emailaddress.py' | |||
259 | --- identityprovider/models/emailaddress.py 2012-11-29 14:47:09 +0000 | |||
260 | +++ identityprovider/models/emailaddress.py 2012-12-13 20:21:20 +0000 | |||
261 | @@ -7,6 +7,7 @@ | |||
262 | 7 | from django.conf import settings | 7 | from django.conf import settings |
263 | 8 | from django.core.mail import send_mail | 8 | from django.core.mail import send_mail |
264 | 9 | from django.core.urlresolvers import reverse | 9 | from django.core.urlresolvers import reverse |
265 | 10 | from django.core.validators import validate_email | ||
266 | 10 | from django.db import models | 11 | from django.db import models |
267 | 11 | from django.template.defaultfilters import force_escape | 12 | from django.template.defaultfilters import force_escape |
268 | 12 | from django.template.loader import render_to_string | 13 | from django.template.loader import render_to_string |
269 | @@ -22,7 +23,7 @@ | |||
270 | 22 | 23 | ||
271 | 23 | 24 | ||
272 | 24 | class EmailAddress(models.Model): | 25 | class EmailAddress(models.Model): |
274 | 25 | email = models.TextField() | 26 | email = models.TextField(validators=[validate_email]) |
275 | 26 | lp_person = models.ForeignKey( | 27 | lp_person = models.ForeignKey( |
276 | 27 | Person, db_column='person', blank=True, null=True, editable=False) | 28 | Person, db_column='person', blank=True, null=True, editable=False) |
277 | 28 | status = models.IntegerField(choices=EmailStatus._get_choices()) | 29 | status = models.IntegerField(choices=EmailStatus._get_choices()) |
278 | 29 | 30 | ||
279 | === added file 'identityprovider/tests/unit/test_validators.py' | |||
280 | --- identityprovider/tests/unit/test_validators.py 1970-01-01 00:00:00 +0000 | |||
281 | +++ identityprovider/tests/unit/test_validators.py 2012-12-13 20:21:20 +0000 | |||
282 | @@ -0,0 +1,26 @@ | |||
283 | 1 | from django.core.exceptions import ValidationError | ||
284 | 2 | |||
285 | 3 | from identityprovider.tests.utils import SSOBaseTestCase | ||
286 | 4 | from identityprovider.validators import validate_password_policy | ||
287 | 5 | |||
288 | 6 | |||
289 | 7 | class PasswordPolicyValidatorTestCase(SSOBaseTestCase): | ||
290 | 8 | |||
291 | 9 | def test_password_too_short(self): | ||
292 | 10 | self.assertRaises( | ||
293 | 11 | ValidationError, validate_password_policy, 'abcd3Fg') | ||
294 | 12 | |||
295 | 13 | def test_password_missing_uppercase(self): | ||
296 | 14 | self.assertRaises( | ||
297 | 15 | ValidationError, validate_password_policy, 'abcd3fgh') | ||
298 | 16 | |||
299 | 17 | def test_password_missing_number(self): | ||
300 | 18 | self.assertRaises( | ||
301 | 19 | ValidationError, validate_password_policy, 'abcdEfgh') | ||
302 | 20 | |||
303 | 21 | def test_password_invalid_chars(self): | ||
304 | 22 | self.assertRaises( | ||
305 | 23 | ValidationError, validate_password_policy, u'abcd\xe1Fgh') | ||
306 | 24 | |||
307 | 25 | def test_valid_password(self): | ||
308 | 26 | self.assertEqual(validate_password_policy('abcD3fgh'), None) | ||
309 | 0 | 27 | ||
310 | === added file 'identityprovider/validators.py' | |||
311 | --- identityprovider/validators.py 1970-01-01 00:00:00 +0000 | |||
312 | +++ identityprovider/validators.py 2012-12-13 20:21:20 +0000 | |||
313 | @@ -0,0 +1,21 @@ | |||
314 | 1 | from django.core.exceptions import ValidationError | ||
315 | 2 | from django.utils.translation import ugettext_lazy as _ | ||
316 | 3 | |||
317 | 4 | from identityprovider.utils import ( | ||
318 | 5 | password_policy_compliant, | ||
319 | 6 | ) | ||
320 | 7 | |||
321 | 8 | |||
322 | 9 | PASSWORD_POLICY_ERROR = _("Password must be at least " | ||
323 | 10 | "8 characters long, and must contain at least one " | ||
324 | 11 | "number and an upper case letter.") | ||
325 | 12 | |||
326 | 13 | |||
327 | 14 | def validate_password_policy(password): | ||
328 | 15 | """Validate password complies with policy.""" | ||
329 | 16 | try: | ||
330 | 17 | str(password) | ||
331 | 18 | except UnicodeEncodeError: | ||
332 | 19 | raise ValidationError(_("Invalid characters in password")) | ||
333 | 20 | if not password_policy_compliant(password): | ||
334 | 21 | raise ValidationError(PASSWORD_POLICY_ERROR) | ||
335 | 0 | 22 | ||
336 | === modified file 'webui/views/ui.py' | |||
337 | --- webui/views/ui.py 2012-12-06 14:31:23 +0000 | |||
338 | +++ webui/views/ui.py 2012-12-13 20:21:20 +0000 | |||
339 | @@ -48,7 +48,6 @@ | |||
340 | 48 | ) | 48 | ) |
341 | 49 | from identityprovider.models import ( | 49 | from identityprovider.models import ( |
342 | 50 | Account, | 50 | Account, |
343 | 51 | AccountPassword, | ||
344 | 52 | AuthToken, | 51 | AuthToken, |
345 | 53 | AuthTokenFactory, | 52 | AuthTokenFactory, |
346 | 54 | EmailAddress, | 53 | EmailAddress, |
347 | @@ -1023,15 +1022,7 @@ | |||
348 | 1023 | if not created: | 1022 | if not created: |
349 | 1024 | account.preferredemail = email_obj | 1023 | account.preferredemail = email_obj |
350 | 1025 | email = account.preferredemail.email | 1024 | email = account.preferredemail.email |
360 | 1026 | try: | 1025 | account.set_password(password) |
352 | 1027 | password_obj = account.accountpassword | ||
353 | 1028 | except AccountPassword.DoesNotExist: | ||
354 | 1029 | password_obj = AccountPassword.objects.create( | ||
355 | 1030 | account=account, | ||
356 | 1031 | password='invalid', | ||
357 | 1032 | ) | ||
358 | 1033 | password_obj.password = encrypt_launchpad_password(password) | ||
359 | 1034 | password_obj.save() | ||
361 | 1035 | user = auth.authenticate(username=email, password=password) | 1026 | user = auth.authenticate(username=email, password=password) |
362 | 1036 | auth.login(request, user) | 1027 | auth.login(request, user) |
363 | 1037 | atrequest.consume() | 1028 | atrequest.consume() |
Code looks good. This piece seems buggy to me, perhaps you can fix it and add a test for it as well?
108 - 'errors': [PASSWORD_ POLICY_ ERROR]
109 + 'errors': e.message
Also, could you please add a couple of more tests to PasswordPolicyV alidatorTestCas e?