Skip to content

Commit 0a48e31

Browse files
committed
Change password view
1 parent 153003a commit 0a48e31

File tree

9 files changed

+259
-19
lines changed

9 files changed

+259
-19
lines changed

tests/unit/manage/test_forms.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,12 @@ def test_creation(self):
7878
form = forms.AddEmailForm(user_service=user_service)
7979

8080
assert form.user_service is user_service
81+
82+
83+
class TestChangePasswordForm:
84+
85+
def test_creation(self):
86+
user_service = pretend.stub()
87+
form = forms.ChangePasswordForm(user_service=user_service)
88+
89+
assert form.user_service is user_service

tests/unit/manage/test_views.py

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,21 @@ def test_default_response(self, monkeypatch):
4242
save_profile_obj = pretend.stub()
4343
save_profile_cls = pretend.call_recorder(lambda **kw: save_profile_obj)
4444
monkeypatch.setattr(views, 'SaveProfileForm', save_profile_cls)
45+
4546
add_email_obj = pretend.stub()
46-
add_email_cls = pretend.call_recorder(lambda *a, **kw: add_email_obj)
47+
add_email_cls = pretend.call_recorder(lambda **kw: add_email_obj)
4748
monkeypatch.setattr(views, 'AddEmailForm', add_email_cls)
49+
50+
change_pass_obj = pretend.stub()
51+
change_pass_cls = pretend.call_recorder(lambda **kw: change_pass_obj)
52+
monkeypatch.setattr(views, 'ChangePasswordForm', change_pass_cls)
53+
4854
view = views.ManageProfileViews(request)
4955

5056
assert view.default_response == {
5157
'save_profile_form': save_profile_obj,
5258
'add_email_form': add_email_obj,
59+
'change_password_form': change_pass_obj,
5360
}
5461
assert view.request == request
5562
assert view.user_service == user_service
@@ -59,6 +66,9 @@ def test_default_response(self, monkeypatch):
5966
assert add_email_cls.calls == [
6067
pretend.call(user_service=user_service),
6168
]
69+
assert change_pass_cls.calls == [
70+
pretend.call(user_service=user_service),
71+
]
6272

6373
def test_manage_profile(self, monkeypatch):
6474
user_service = pretend.stub()
@@ -386,7 +396,7 @@ def test_reverify_email(self, monkeypatch):
386396
flash=pretend.call_recorder(lambda *a, **kw: None)
387397
),
388398
find_service=lambda *a, **kw: pretend.stub(),
389-
user=pretend.stub(id=pretend.stub),
399+
user=pretend.stub(id=pretend.stub()),
390400
)
391401
send_email = pretend.call_recorder(lambda *a: None)
392402
monkeypatch.setattr(views, 'send_email_verification_email', send_email)
@@ -419,7 +429,7 @@ def raise_no_result():
419429
flash=pretend.call_recorder(lambda *a, **kw: None)
420430
),
421431
find_service=lambda *a, **kw: pretend.stub(),
422-
user=pretend.stub(id=pretend.stub),
432+
user=pretend.stub(id=pretend.stub()),
423433
)
424434
send_email = pretend.call_recorder(lambda *a: None)
425435
monkeypatch.setattr(views, 'send_email_verification_email', send_email)
@@ -448,7 +458,7 @@ def test_reverify_email_already_verified(self, monkeypatch):
448458
flash=pretend.call_recorder(lambda *a, **kw: None)
449459
),
450460
find_service=lambda *a, **kw: pretend.stub(),
451-
user=pretend.stub(id=pretend.stub),
461+
user=pretend.stub(id=pretend.stub()),
452462
)
453463
send_email = pretend.call_recorder(lambda *a: None)
454464
monkeypatch.setattr(views, 'send_email_verification_email', send_email)
@@ -463,6 +473,100 @@ def test_reverify_email_already_verified(self, monkeypatch):
463473
]
464474
assert send_email.calls == []
465475

476+
def test_change_password(self, monkeypatch):
477+
old_password = '0ld_p455w0rd'
478+
new_password = 'n3w_p455w0rd'
479+
user_service = pretend.stub(
480+
update_user=pretend.call_recorder(lambda *a, **kw: None)
481+
)
482+
request = pretend.stub(
483+
POST={
484+
'password': old_password,
485+
'new_password': new_password,
486+
'password_confirm': new_password,
487+
},
488+
session=pretend.stub(
489+
flash=pretend.call_recorder(lambda *a, **kw: None)
490+
),
491+
find_service=lambda *a, **kw: user_service,
492+
user=pretend.stub(
493+
id=pretend.stub(),
494+
username=pretend.stub(),
495+
email=pretend.stub(),
496+
name=pretend.stub(),
497+
),
498+
)
499+
change_pwd_obj = pretend.stub(
500+
validate=lambda: True,
501+
new_password=pretend.stub(data=new_password),
502+
)
503+
change_pwd_cls = pretend.call_recorder(lambda *a, **kw: change_pwd_obj)
504+
monkeypatch.setattr(views, 'ChangePasswordForm', change_pwd_cls)
505+
506+
send_email = pretend.call_recorder(lambda *a: None)
507+
monkeypatch.setattr(views, 'send_password_change_email', send_email)
508+
monkeypatch.setattr(
509+
views.ManageProfileViews, 'default_response', {'_': pretend.stub()}
510+
)
511+
view = views.ManageProfileViews(request)
512+
513+
assert view.change_password() == {
514+
**view.default_response,
515+
'change_password_form': change_pwd_obj,
516+
}
517+
assert request.session.flash.calls == [
518+
pretend.call('Password updated.', queue='success'),
519+
]
520+
assert send_email.calls == [pretend.call(request, request.user)]
521+
assert user_service.update_user.calls == [
522+
pretend.call(request.user.id, password=new_password),
523+
]
524+
525+
def test_change_password_validation_fails(self, monkeypatch):
526+
old_password = '0ld_p455w0rd'
527+
new_password = 'n3w_p455w0rd'
528+
user_service = pretend.stub(
529+
update_user=pretend.call_recorder(lambda *a, **kw: None)
530+
)
531+
request = pretend.stub(
532+
POST={
533+
'password': old_password,
534+
'new_password': new_password,
535+
'password_confirm': new_password,
536+
},
537+
session=pretend.stub(
538+
flash=pretend.call_recorder(lambda *a, **kw: None)
539+
),
540+
find_service=lambda *a, **kw: user_service,
541+
user=pretend.stub(
542+
id=pretend.stub(),
543+
username=pretend.stub(),
544+
email=pretend.stub(),
545+
name=pretend.stub(),
546+
),
547+
)
548+
change_pwd_obj = pretend.stub(
549+
validate=lambda: False,
550+
new_password=pretend.stub(data=new_password),
551+
)
552+
change_pwd_cls = pretend.call_recorder(lambda *a, **kw: change_pwd_obj)
553+
monkeypatch.setattr(views, 'ChangePasswordForm', change_pwd_cls)
554+
555+
send_email = pretend.call_recorder(lambda *a: None)
556+
monkeypatch.setattr(views, 'send_password_change_email', send_email)
557+
monkeypatch.setattr(
558+
views.ManageProfileViews, 'default_response', {'_': pretend.stub()}
559+
)
560+
view = views.ManageProfileViews(request)
561+
562+
assert view.change_password() == {
563+
**view.default_response,
564+
'change_password_form': change_pwd_obj,
565+
}
566+
assert request.session.flash.calls == []
567+
assert send_email.calls == []
568+
assert user_service.update_user.calls == []
569+
466570

467571
class TestManageProjects:
468572

tests/unit/test_email.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,47 @@ def test_email_verification_email(
217217
assert send_email.delay.calls == [
218218
pretend.call('Email Body', [stub_email.email], 'Email Subject'),
219219
]
220+
221+
222+
class TestPasswordChangeEmail:
223+
224+
def test_password_change_email(
225+
self, pyramid_request, pyramid_config, monkeypatch):
226+
227+
stub_user = pretend.stub(
228+
email='email',
229+
username='username',
230+
)
231+
subject_renderer = pyramid_config.testing_add_renderer(
232+
'email/password-change.subject.txt'
233+
)
234+
subject_renderer.string_response = 'Email Subject'
235+
body_renderer = pyramid_config.testing_add_renderer(
236+
'email/password-change.body.txt'
237+
)
238+
body_renderer.string_response = 'Email Body'
239+
240+
send_email = pretend.stub(
241+
delay=pretend.call_recorder(lambda *args, **kwargs: None)
242+
)
243+
pyramid_request.task = pretend.call_recorder(
244+
lambda *args, **kwargs: send_email
245+
)
246+
monkeypatch.setattr(email, 'send_email', send_email)
247+
248+
result = email.send_password_change_email(
249+
pyramid_request,
250+
user=stub_user,
251+
)
252+
253+
assert result == {
254+
'username': stub_user.username,
255+
}
256+
subject_renderer.assert_()
257+
body_renderer.assert_(username=stub_user.username)
258+
assert pyramid_request.task.calls == [
259+
pretend.call(send_email),
260+
]
261+
assert send_email.delay.calls == [
262+
pretend.call('Email Body', [stub_user.email], 'Email Subject'),
263+
]

warehouse/email.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,11 @@ def send_password_reset_email(request, user):
5151
}
5252

5353
subject = render(
54-
'email/password-reset.subject.txt',
55-
fields,
56-
request=request,
54+
'email/password-reset.subject.txt', fields, request=request
5755
)
5856

5957
body = render(
60-
'email/password-reset.body.txt',
61-
fields,
62-
request=request,
58+
'email/password-reset.body.txt', fields, request=request
6359
)
6460

6561
request.task(send_email).delay(body, [user.email], subject)
@@ -84,17 +80,31 @@ def send_email_verification_email(request, email):
8480
}
8581

8682
subject = render(
87-
'email/verify-email.subject.txt',
88-
fields,
89-
request=request,
83+
'email/verify-email.subject.txt', fields, request=request
9084
)
9185

9286
body = render(
93-
'email/verify-email.body.txt',
94-
fields,
95-
request=request,
87+
'email/verify-email.body.txt', fields, request=request
9688
)
9789

9890
request.task(send_email).delay(body, [email.email], subject)
9991

10092
return fields
93+
94+
95+
def send_password_change_email(request, user):
96+
fields = {
97+
'username': user.username,
98+
}
99+
100+
subject = render(
101+
'email/password-change.subject.txt', fields, request=request
102+
)
103+
104+
body = render(
105+
'email/password-change.body.txt', fields, request=request
106+
)
107+
108+
request.task(send_email).delay(body, [user.email], subject)
109+
110+
return fields

warehouse/manage/forms.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
import wtforms
1414

1515
from warehouse import forms
16-
from warehouse.accounts.forms import NewEmailMixin
16+
from warehouse.accounts.forms import (
17+
NewEmailMixin, NewPasswordMixin, PasswordMixin,
18+
)
1719

1820

1921
class RoleNameMixin:
@@ -72,3 +74,12 @@ class AddEmailForm(NewEmailMixin, forms.Form):
7274
def __init__(self, *args, user_service, **kwargs):
7375
super().__init__(*args, **kwargs)
7476
self.user_service = user_service
77+
78+
79+
class ChangePasswordForm(PasswordMixin, NewPasswordMixin, forms.Form):
80+
81+
__params__ = ['password', 'new_password', 'password_confirm']
82+
83+
def __init__(self, *args, user_service, **kwargs):
84+
super().__init__(*args, **kwargs)
85+
self.user_service = user_service

warehouse/manage/views.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020

2121
from warehouse.accounts.interfaces import IUserService
2222
from warehouse.accounts.models import User, Email
23-
from warehouse.email import send_email_verification_email
23+
from warehouse.email import (
24+
send_email_verification_email, send_password_change_email,
25+
)
2426
from warehouse.manage.forms import (
25-
AddEmailForm, CreateRoleForm, ChangeRoleForm, SaveProfileForm
27+
AddEmailForm, ChangePasswordForm, CreateRoleForm, ChangeRoleForm,
28+
SaveProfileForm,
2629
)
2730
from warehouse.packaging.models import JournalEntry, Role, File
2831
from warehouse.utils.project import confirm_project, remove_project
@@ -46,6 +49,9 @@ def default_response(self):
4649
return {
4750
'save_profile_form': SaveProfileForm(name=self.request.user.name),
4851
'add_email_form': AddEmailForm(user_service=self.user_service),
52+
'change_password_form': ChangePasswordForm(
53+
user_service=self.user_service
54+
),
4955
}
5056

5157
@view_config(request_method="GET")
@@ -189,6 +195,34 @@ def reverify_email(self):
189195

190196
return self.default_response
191197

198+
@view_config(
199+
request_method='POST',
200+
request_param=ChangePasswordForm.__params__,
201+
)
202+
def change_password(self):
203+
form = ChangePasswordForm(
204+
**self.request.POST,
205+
username=self.request.user.username,
206+
full_name=self.request.user.name,
207+
email=self.request.user.email,
208+
user_service=self.user_service,
209+
)
210+
211+
if form.validate():
212+
self.user_service.update_user(
213+
self.request.user.id,
214+
password=form.new_password.data,
215+
)
216+
send_password_change_email(self.request, self.request.user)
217+
self.request.session.flash(
218+
'Password updated.', queue='success'
219+
)
220+
221+
return {
222+
**self.default_response,
223+
'change_password_form': form,
224+
}
225+
192226

193227
@view_config(
194228
route_name="manage.projects",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Someone, perhaps you, has changed the password for your PyPI account
2+
'{{ username }}'.
3+
4+
If you did not make this change, you can reply to this email directly to
5+
communicate with the PyPI administrators.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
PyPI Password Change Notification

warehouse/templates/manage/profile.html

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,26 @@ <h2>Account Emails</h2>
207207
</div>
208208
</div>
209209
</form>
210+
211+
{{ form_error_anchor(change_password_form) }}
212+
<h2>Change password</h2>
213+
<form method="POST" action="#errors">
214+
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
215+
{{ form_errors(change_password_form) }}
216+
<div class="form-group">
217+
<label class="form-group__label" for="name">Old password</label>
218+
{{ change_password_form.password }}
219+
{{ field_errors(change_password_form.password) }}
220+
<label class="form-group__label" for="name">New password</label>
221+
{{ change_password_form.new_password }}
222+
{{ field_errors(change_password_form.new_password) }}
223+
<label class="form-group__label" for="name">Confirm new password</label>
224+
{{ change_password_form.password_confirm }}
225+
{{ field_errors(change_password_form.password_confirm) }}
226+
</div>
227+
<input value="Update password" class="button button--primary" type="submit">
228+
<a href="{{ request.route_path("accounts.request-password-reset") }}">
229+
I forgot my password
230+
</a>
231+
</form>
210232
{% endblock %}

0 commit comments

Comments
 (0)