Browse Source

added account settings and change password

rework
Holger Frey 7 years ago
parent
commit
5cfd68e85e
  1. 2
      Makefile
  2. 13
      ordr/events.py
  3. 1
      ordr/models/account.py
  4. 7
      ordr/resources/__init__.py
  5. 81
      ordr/resources/account.py
  6. 2
      ordr/schemas/__init__.py
  7. 49
      ordr/schemas/account.py
  8. 0
      ordr/schemas/validators.py
  9. 16
      ordr/templates/account/password_changed.jinja2
  10. 14
      ordr/templates/account/password_form.jinja2
  11. 14
      ordr/templates/account/settings_form.jinja2
  12. 16
      ordr/templates/account/settings_mail_changed.jinja2
  13. 18
      ordr/templates/deform/password.pt
  14. 2
      ordr/templates/deform/textinput.pt
  15. 25
      ordr/templates/emails/email_change.jinja2
  16. 8
      ordr/templates/layout.jinja2
  17. 2
      ordr/templates/pages/login.jinja2
  18. 142
      ordr/views/account.py
  19. 4
      tests/__init__.py
  20. 132
      tests/_functional/account.py
  21. 2
      tests/_functional/layout.py
  22. 15
      tests/events.py
  23. 120
      tests/resources/account.py
  24. 9
      tests/resources/root.py
  25. 20
      tests/schemas/validators.py
  26. 311
      tests/views/account.py
  27. 6
      tests/views/forgotten_password.py
  28. 6
      tests/views/registration.py

2
Makefile

@ -53,7 +53,7 @@ lint: ## check style with flake8 @@ -53,7 +53,7 @@ lint: ## check style with flake8
flake8 ordr tests
test: ## run tests quickly with the default Python, ignoring functional tests
py.test
py.test -x
coverage: ## check code coverage quickly with the default Python
coverage run --source ordr -m pytest --ignore tests/_functional/

13
ordr/events.py

@ -14,6 +14,7 @@ class UserNotification(object): @@ -14,6 +14,7 @@ class UserNotification(object):
:param pyramid.request.Request request: current request object
:param ordr.models.account.Users account: send notification to this user
:param dict data: additional data to pass to the mail template
:param str send_to: optional email address overriding user's email address
'''
#: subject of the notification
@ -22,10 +23,11 @@ class UserNotification(object): @@ -22,10 +23,11 @@ class UserNotification(object):
#: template to render
template = None
def __init__(self, request, account, data=None):
def __init__(self, request, account, data=None, send_to=None):
self.request = request
self.account = account
self.data = data
self.send_to = send_to or account.email
class ActivationNotification(UserNotification):
@ -35,6 +37,13 @@ class ActivationNotification(UserNotification): @@ -35,6 +37,13 @@ class ActivationNotification(UserNotification):
template = 'ordr:templates/emails/activation.jinja2'
class ChangeEmailNotification(UserNotification):
''' user notification for verifying a change of the mail address '''
subject = '[ordr] Verify New Email Address'
template = 'ordr:templates/emails/email_change.jinja2'
class OrderStatusNotification(UserNotification):
''' user notification for order status change '''
@ -69,7 +78,7 @@ def notify_user(event): @@ -69,7 +78,7 @@ def notify_user(event):
message = Message(
subject=event.subject,
sender=event.request.registry.settings['mail.default_sender'],
recipients=[event.account.email],
recipients=[event.send_to],
html=body
)
mailer = get_mailer(event.request.registry)

1
ordr/models/account.py

@ -53,6 +53,7 @@ class TokenSubject(enum.Enum): @@ -53,6 +53,7 @@ class TokenSubject(enum.Enum):
REGISTRATION = 'registration' #: validate email for new user
RESET_PASSWORD = 'reset_password' #: reset a forgotten password
CHANGE_EMAIL = 'change_email' #: validate an email change
# database driven models

7
ordr/resources/__init__.py

@ -2,7 +2,11 @@ @@ -2,7 +2,11 @@
from pyramid.security import Allow, Everyone, DENY_ALL
from .account import RegistrationResource, PasswordResetResource
from .account import (
RegistrationResource,
PasswordResetResource,
AccountResource
)
class RootResource:
@ -36,6 +40,7 @@ class RootResource: @@ -36,6 +40,7 @@ class RootResource:
map = {
'register': RegistrationResource,
'forgot': PasswordResetResource,
'account': AccountResource,
}
child_class = map[key]
return child_class(name=key, parent=self)

81
ordr/resources/account.py

@ -2,10 +2,16 @@ @@ -2,10 +2,16 @@
import deform
from pyramid.security import Allow, Everyone, DENY_ALL
from pyramid.security import Allow, Authenticated, Everyone, DENY_ALL
from ordr.models.account import Token, TokenSubject
from ordr.schemas.account import RegistrationSchema, ResetPasswordSchema
from ordr.schemas.account import (
ChangePasswordSchema,
RegistrationSchema,
ResetPasswordSchema,
SettingsSchema
)
from .helpers import BaseChildResource
@ -109,3 +115,74 @@ class PasswordResetResource(BaseChildResource): @@ -109,3 +115,74 @@ class PasswordResetResource(BaseChildResource):
if token is None:
raise KeyError(f'Token {key} not found')
return PasswordResetTokenResource(name=key, parent=self, model=token)
class ChangeEmailTokenResource(BaseChildResource):
''' Resource for changing the email address
:param pyramid.request.Request request: the current request object
:param str name: the name of the resource
:param parent: the parent resouce
'''
nav_active = None
def __acl__(self):
''' access controll list for the resource '''
return [(Allow, self.model.owner.principal, 'edit'), DENY_ALL]
class AccountResource(BaseChildResource):
''' The resource for changing account settings and passwords
:param pyramid.request.Request request: the current request object
:param str name: the name of the resource
:param parent: the parent resouce
'''
nav_active = None
def __init__(self, name, parent, model=None):
''' Create the resource
:param str name: the name of the resource
:param parent: the parent resouce
:param model: optional data model for the resource
If model is not set, the current user will be used
'''
super().__init__(name, parent, model)
self.model = model or getattr(self.request, 'user', None)
def __acl__(self):
''' access controll list for the resource '''
return [(Allow, Authenticated, 'edit'), DENY_ALL]
def __getitem__(self, key):
''' returns a resource for a valid change email token '''
token = Token.retrieve(self.request, key, TokenSubject.CHANGE_EMAIL)
if token is None:
raise KeyError(f'Token {key} not found')
return ChangeEmailTokenResource(name=key, parent=self, model=token)
def get_settings_form(self, **kwargs):
''' returns the account settings form '''
settings = {
'buttons': (
deform.Button(name='change', title='Change Settings'),
deform.Button(name='cancel', title='Cancel'),
)
}
settings.update(kwargs)
return self._prepare_form(SettingsSchema, **settings)
def get_password_form(self, **kwargs):
''' returns the change password form '''
settings = {
'buttons': (
deform.Button(name='change', title='Change Password'),
deform.Button(name='cancel', title='Cancel'),
)
}
settings.update(kwargs)
return self._prepare_form(ChangePasswordSchema, **settings)

2
ordr/schemas/__init__.py

@ -5,7 +5,7 @@ import deform @@ -5,7 +5,7 @@ import deform
from deform.renderer import configure_zpt_renderer
from .helpers import (
from .validators import (
deferred_csrf_default,
deferred_csrf_validator
)

49
ordr/schemas/account.py

@ -3,9 +3,10 @@ import deform @@ -3,9 +3,10 @@ import deform
from . import CSRFSchema
from .helpers import (
from .validators import (
deferred_unique_email_validator,
deferred_unique_username_validator,
deferred_password_validator
)
@ -47,10 +48,54 @@ class RegistrationSchema(CSRFSchema): @@ -47,10 +48,54 @@ class RegistrationSchema(CSRFSchema):
class ResetPasswordSchema(CSRFSchema):
''' reset a forgotten password registration '''
''' reset a forgotten password '''
password = colander.SchemaNode(
colander.String(),
widget=deform.widget.CheckedPasswordWidget(),
validator=colander.Length(min=8)
)
class SettingsSchema(CSRFSchema):
''' new user registration '''
username = colander.SchemaNode(
colander.String(),
widget=deform.widget.TextInputWidget(readonly=True)
)
first_name = colander.SchemaNode(
colander.String()
)
last_name = colander.SchemaNode(
colander.String()
)
email = colander.SchemaNode(
colander.String(),
validator=deferred_unique_email_validator
)
confirmation = colander.SchemaNode(
colander.String(),
widget=deform.widget.PasswordWidget(),
validator=deferred_password_validator
)
class ChangePasswordSchema(CSRFSchema):
''' change the password '''
password = colander.SchemaNode(
colander.String(),
widget=deform.widget.CheckedPasswordWidget(),
validator=colander.Length(min=8)
)
confirmation = colander.SchemaNode(
colander.String(),
widget=deform.widget.PasswordWidget(),
validator=deferred_password_validator
)

0
ordr/schemas/helpers.py → ordr/schemas/validators.py

16
ordr/templates/account/password_changed.jinja2

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
{% extends "ordr:templates/layout.jinja2" %}
{% block content %}
<div class="row justify-content-md-center mt-3">
<div class="col-6">
<h1>Change Your Password</h1>
</div>
</div>
<div class="row justify-content-md-center">
<div class="col-6">
<h3>Your password was changed successfully</h3>
<p class="mt-3">You can now log in with your new password.</p>
<p>Happy <a href="{{ request.resource_url(request.root) }}">ordering</a>
</div>
</div>
{% endblock content %}

14
ordr/templates/account/password_form.jinja2

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
{% extends "ordr:templates/layout.jinja2" %}
{% block content %}
<div class="row justify-content-md-center mt-3">
<div class="col-6">
<h1>Change Your Password</h1>
</div>
</div>
<div class="row justify-content-md-center">
<div class="col-6 mt-3">
{{ form.render()|safe }}
</div>
</div>
{% endblock content %}

14
ordr/templates/account/settings_form.jinja2

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
{% extends "ordr:templates/layout.jinja2" %}
{% block content %}
<div class="row justify-content-md-center mt-3">
<div class="col-6">
<h1>Change Settings</h1>
</div>
</div>
<div class="row justify-content-md-center">
<div class="col-6 mt-3">
{{ form.render()|safe }}
</div>
</div>
{% endblock content %}

16
ordr/templates/account/settings_mail_changed.jinja2

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
{% extends "ordr:templates/layout.jinja2" %}
{% block content %}
<div class="row justify-content-md-center mt-3">
<div class="col-6">
<h1>Change Settings</h1>
</div>
</div>
<div class="row justify-content-md-center">
<div class="col-6">
<h3>Your email was changed successfully</h3>
<p class="mt-3">New notifications will be sent to {{request.user.email}}.</p>
<p>Happy <a href="{{ request.resource_url(request.root) }}">ordering</a>
</div>
</div>
{% endblock content %}

18
ordr/templates/deform/password.pt

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
<span tal:define="name name|field.name;
css_class css_class|field.widget.css_class;
oid oid|field.oid;
required required|'required' if field.required else None;
mask mask|field.widget.mask;
mask_placeholder mask_placeholder|field.widget.mask_placeholder;
style style|field.widget.style;
was_validated True if field.get_root().error else False;
is_invalid is_invalid|field.error and not field.widget.hidden and not field.typ.__class__.__name__=='Mapping';
is_valid was_validated and not is_invalid;
"
tal:omit-tag="">
<input type="password" name="${name}" value="${cstruct}"
tal:attributes="class string: form-control ${css_class or ''} ${'is-invalid' if is_invalid else ''} ${'is-valid' if is_valid else ''};
style style;
required required"
id="${oid}"/>
</span>

2
ordr/templates/deform/textinput.pt

@ -1,10 +1,10 @@ @@ -1,10 +1,10 @@
<span tal:define="name name|field.name;
css_class css_class|field.widget.css_class;
oid oid|field.oid;
required required|'required' if field.required else None;
mask mask|field.widget.mask;
mask_placeholder mask_placeholder|field.widget.mask_placeholder;
style style|field.widget.style;
required required|'required' if field.required else None;
was_validated True if field.get_root().error else False;
is_invalid is_invalid|field.error and not field.widget.hidden and not field.typ.__class__.__name__=='Mapping';
is_valid was_validated and not is_invalid;

25
ordr/templates/emails/email_change.jinja2

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>[ordr] verify your new email address</title>
<link href='http://fonts.googleapis.com/css?family=Anton&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
</head>
<body>
<h1>Hi there!</h1>
<p>
Please verify your new email address for the account "{{ user.username }}" by following this link
<a href="{{ request.resource_url(context, data.token.hash) }}">{{ request.resource_url(context, data.token.hash) }}</a>
</p>
<p> The link will expire on {{ data.token.expires.strftime('%d.%m.%y at %H:%M') }}.
<p class="signature">
Regards,
<br/>
<span class="brand">ordr</span>
</p>
<p class="footprint">
<small>Please don't respont to this email! This is an automatically generated notification by the system.</small>
<a href="http://distractedbysquirrels.com/" target="_blank" title="This software was originally written by Sebastian Sebald." class="icon-dbs"></a>
</p>
</body>
</html>

8
ordr/templates/layout.jinja2

@ -51,19 +51,19 @@ @@ -51,19 +51,19 @@
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDrowpdown" role="button" data-toggle="dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" >
{{ request.user }}
</a>
<div class="dropdown-menu" aria-labelledby="userDropdown">
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="userDropdown">
<a class="dropdown-item" href="/logout">Logout</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/account">Settings</a>
<a class="dropdown-item small" href="/account/settings">Settings</a>
<a class="dropdown-item small" href="/account/password">Change Password</a>
</div>
</li>
</ul>
{% endif %}
</nav>
<div class="container-fluid content">
{% block content %}
<p>No content</p>

2
ordr/templates/pages/login.jinja2

@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
<input type="text" class="form-control {% if loginerror %}is-invalid{% endif %}" id="input-username" placeholder="Username" name="username" autofocus="autofocus">
</div>
<div class="form-group">
<input type="text" class="form-control {% if loginerror %}is-invalid{% endif %}" id="input-password" placeholder="Password" name="password">
<input type="password" class="form-control {% if loginerror %}is-invalid{% endif %}" id="input-password" placeholder="Password" name="password">
{% if loginerror %}
<div class="invalid-feedback">
Username and password do not match, or account is not activated.

142
ordr/views/account.py

@ -0,0 +1,142 @@ @@ -0,0 +1,142 @@
import deform
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
from ordr.events import ChangeEmailNotification
from ordr.models.account import TokenSubject
@view_config(
context='ordr.resources.account.AccountResource',
permission='edit'
)
def account(context, request):
''' redirect if '/account' was requested directly '''
return HTTPFound(request.resource_url(request.root))
@view_config(
context='ordr.resources.account.AccountResource',
permission='edit',
name='settings',
request_method='GET',
renderer='ordr:templates/account/settings_form.jinja2'
)
def settings_form(context, request):
''' show the settings form '''
prefill = {
'username': request.user.username,
'first_name': request.user.first_name,
'last_name': request.user.last_name,
'email': request.user.email,
}
form = context.get_settings_form(prefill=prefill)
return {'form': form}
@view_config(
context='ordr.resources.account.AccountResource',
permission='edit',
name='settings',
request_method='POST',
renderer='ordr:templates/account/settings_form.jinja2'
)
def settings_form_processing(context, request):
''' process the settings form '''
if 'change' not in request.POST:
return HTTPFound(request.resource_url(request.root))
form = context.get_settings_form()
data = request.POST.items()
try:
appstruct = form.validate(data)
except deform.ValidationFailure as e:
return {'form': form}
# form validation successfull, change user
request.user.first_name = appstruct['first_name']
request.user.last_name = appstruct['last_name']
if appstruct['email'] == request.user.email:
# email was not changed
return HTTPFound(request.resource_url(request.root))
# create a verify-new-email token and send email
token = request.user.issue_token(
request,
TokenSubject.CHANGE_EMAIL,
payload={'email': appstruct['email']}
)
notification = ChangeEmailNotification(
request,
account,
{'token': token},
send_to=appstruct['email']
)
request.registry.notify(notification)
return HTTPFound(request.resource_url(context, 'verify'))
@view_config(
context='ordr.resources.account.ChangeEmailTokenResource',
permission='edit',
request_method='GET',
renderer='ordr:templates/account/settings_mail_changed.jinja2'
)
def verify_email(context, request):
''' show email verification text '''
payload = context.model.payload
request.user.email = payload['email']
request.dbsession.delete(context.model)
return {}
@view_config(
context='ordr.resources.account.AccountResource',
permission='edit',
name='password',
request_method='GET',
renderer='ordr:templates/account/password_form.jinja2'
)
def password_form(context, request):
''' show the change password form '''
form = context.get_password_form()
return {'form': form}
@view_config(
context='ordr.resources.account.AccountResource',
permission='edit',
name='password',
request_method='POST',
renderer='ordr:templates/account/password_form.jinja2'
)
def password_form_processing(context, request):
''' process the change password form '''
if 'change' not in request.POST:
return HTTPFound(request.resource_url(request.root))
form = context.get_password_form()
data = request.POST.items()
try:
appstruct = form.validate(data)
except deform.ValidationFailure as e:
return {'form': form}
# form validation successfull, change the password
request.user.set_password(appstruct['password'])
return HTTPFound(request.resource_url(context, 'changed'))
@view_config(
context='ordr.resources.account.AccountResource',
permission='edit',
name='changed',
request_method='GET',
renderer='ordr:templates/account/password_changed.jinja2'
)
def password_changed(context, request):
''' the password changed message '''
return {}

4
tests/__init__.py

@ -79,11 +79,11 @@ def get_example_user(role): @@ -79,11 +79,11 @@ def get_example_user(role):
return user
def get_post_request(dbsession, data):
def get_post_request(data, **kwargs):
''' returns a dummy request with csrf_token for validating deform forms '''
request = testing.DummyRequest()
post_data = {'csrf_token': get_csrf_token(request)}
post_data.update(data)
return testing.DummyRequest(dbsession=dbsession, POST=post_data)
return testing.DummyRequest(POST=post_data, **kwargs)

132
tests/_functional/account.py

@ -0,0 +1,132 @@ @@ -0,0 +1,132 @@
''' functional tests for ordr2.views.account.py '''
from pyramid_mailer import get_mailer
from . import testappsetup, testapp, get_token_url # noqa: F401
def test_account_root(testapp): # noqa: F811
''' check the redirect if '/account' is requested '''
testapp.login('TerryGilliam', 'Terry')
response = testapp.get('/account')
assert response.location == 'http://localhost/'
def test_account_change_settings(testapp): # noqa: F811
testapp.login('TerryGilliam', 'Terry')
response = testapp.get('/account/settings')
active_nav = response.html.find('li', class_='active')
assert active_nav is None
assert 'Change Settings' in response
assert 'value="gilliam@example.com"' in response
assert 'Wrong Password' not in response
# fill out the form without confirmation password
form = response.form
form['first_name'] = 'Amy'
form['last_name'] = 'McDonald'
response = form.submit(name='change')
active_nav = response.html.find('li', class_='active')
assert active_nav is None
assert 'Change Settings' in response
assert 'required' in response
# fill out the form with invalid data but correct password
response = testapp.get('/account/settings')
form = response.form
form['first_name'] = 'Amy'
form['last_name'] = 'McDonald'
form['email'] = 'this is not an email address'
form['confirmation'] = 'Terry'
response = form.submit(name='change')
active_nav = response.html.find('li', class_='active')
assert active_nav is None
assert 'Change Settings' in response
assert 'Invalid email address' in response
# fill out the form with valid data and correct password
response = testapp.get('/account/settings')
form = response.form
form['first_name'] = 'Amy'
form['last_name'] = 'McDonald'
form['confirmation'] = 'Terry'
response = form.submit(name='change')
assert response.location == 'http://localhost/'
response = testapp.get('/account/settings')
assert 'value="Amy"' in response
def test_account_change_email(testapp): # noqa: F811
testapp.login('TerryGilliam', 'Terry')
response = testapp.get('/account/settings')
# fill out the form with valid data and correct password
form = response.form
form['email'] = 'amy@example.com'
form['confirmation'] = 'Terry'
response = form.submit(name='change')
assert response.location == 'http://localhost/account/verify'
# click the email verification token
mailer = get_mailer(testapp.app.registry)
email = mailer.outbox[-1]
assert email.subject == '[ordr] Verify New Email Address'
assert email.recipients == ['amy@example.com']
token_link = get_token_url(email, prefix='/account/')
response = testapp.get(token_link)
active_nav = response.html.find('li', class_='active')
assert active_nav is None
assert 'Change Settings' in response
assert 'changed sucessfully' not in response
def test_account_change_password(testapp): # noqa: F811
testapp.login('TerryGilliam', 'Terry')
response = testapp.get('/account/password')
active_nav = response.html.find('li', class_='active')
assert active_nav is None
assert 'Change Password' in response
assert 'Wrong Password' not in response
# fill out the form with incorrect confirmation password
form = response.form
form['password'] = 'Lost in La Mancha'
form['password-confirm'] = 'Lost in La Mancha'
form['confirmation'] = 'Unknown Password'
response = form.submit(name='change')
active_nav = response.html.find('li', class_='active')
assert active_nav is None
assert 'Change Password' in response
assert 'Wrong password' in response
# fill out the form with invalid data but correct password
response = testapp.get('/account/password')
form = response.form
form['password'] = 'Lost in La Mancha'
form['password-confirm'] = 'confirmation does not match'
form['confirmation'] = 'Terry'
response = form.submit(name='change')
active_nav = response.html.find('li', class_='active')
assert active_nav is None
assert 'Change Password' in response
assert 'Password did not match confirm' in response
# fill out the form with valid data and correct password
response = testapp.get('/account/password')
form = response.form
form['password'] = 'Lost in La Mancha'
form['password-confirm'] = 'Lost in La Mancha'
form['confirmation'] = 'Terry'
response = form.submit(name='change')
assert response.location == 'http://localhost/account/changed'
response = response.follow()
active_nav = response.html.find('li', class_='active')
assert active_nav is None
assert 'Your password was changed successfully' in response
assert testapp.login('TerryGilliam', 'Lost in La Mancha')

2
tests/_functional/layout.py

@ -36,7 +36,7 @@ def test_navbar_with_user(testapp, username, password, extras): @@ -36,7 +36,7 @@ def test_navbar_with_user(testapp, username, password, extras):
hrefs = [a['href'] for a in navbar.find_all('a')]
expected = ['/', '/orders', '/faq']
expected.extend(extras)
expected.extend(['#', '/logout', '/account'])
expected.extend(['#', '/logout', '/account/settings', '/account/password'])
assert expected == hrefs
assert 'nav-item dropdown' in response

15
tests/events.py

@ -18,6 +18,21 @@ def test_user_notification_init(app_config): # noqa: F811 @@ -18,6 +18,21 @@ def test_user_notification_init(app_config): # noqa: F811
assert notification.request == 'request'
assert notification.account == user
assert notification.data == 'data'
assert notification.send_to == user.email
def test_user_notification_init_send_to_override(app_config): # noqa: F811
''' test creation of user notification events '''
from ordr.events import UserNotification
from ordr.models.account import Role
user = get_example_user(Role.USER)
notification = UserNotification('request', user, 'data', 'amy@example.com')
assert notification.request == 'request'
assert notification.account == user
assert notification.data == 'data'
assert notification.send_to == 'amy@example.com'
def test_notify_user(app_config): # noqa: F811

120
tests/resources/account.py

@ -31,7 +31,6 @@ def test_registration_acl(): @@ -31,7 +31,6 @@ def test_registration_acl():
def test_registration_get_registration_form():
''' test 'get_registration_form()' method of RegistrationResource '''
from pyramid.security import Allow, Everyone, DENY_ALL
from ordr.resources.account import RegistrationResource
import deform
@ -170,3 +169,122 @@ def test_password_reset_getitem_not_found(dbsession): # noqa: F811 @@ -170,3 +169,122 @@ def test_password_reset_getitem_not_found(dbsession): # noqa: F811
with pytest.raises(KeyError):
resource['unknown hash']
def test_change_email_reset_token_acl(dbsession): # noqa: F811
''' test access controll list for PasswordResetTokenResource '''
from pyramid.security import Allow, DENY_ALL
from ordr.models.account import Role, Token, TokenSubject
from ordr.resources.account import ChangeEmailTokenResource
request = DummyRequest()
user = get_example_user(Role.USER)
dbsession.add(user)
user.issue_token(request, TokenSubject.CHANGE_EMAIL)
dbsession.flush()
token = dbsession.query(Token).first()
parent = DummyResource(request='request')
resource = ChangeEmailTokenResource('name', parent, model=token)
assert resource.__acl__() == [(Allow, 'user:3', 'edit'), DENY_ALL]
def test_account_resource_set_model_from_request():
''' test access controll list for PasswordResetResource '''
from ordr.resources.account import AccountResource
request = DummyRequest(user='Amy McDonald')
parent = DummyResource(request=request)
resource = AccountResource('a name', parent)
assert resource.model == 'Amy McDonald'
def test_account_resource_acl():
''' test access controll list for PasswordResetResource '''
from pyramid.security import Allow, Authenticated, DENY_ALL
from ordr.resources.account import AccountResource
request = DummyRequest()
parent = DummyResource(request=request)
resource = AccountResource('a name', parent)
assert resource.__acl__() == [(Allow, Authenticated, 'edit'), DENY_ALL]
def test_account_resource_getitem_found(dbsession): # noqa: F811
''' test '__getitem__()' method returns child resource '''
from ordr.models.account import Role, TokenSubject
from ordr.resources.account import (
AccountResource,
ChangeEmailTokenResource
)
request = DummyRequest(dbsession=dbsession)
user = get_example_user(Role.NEW)
token = user.issue_token(request, TokenSubject.CHANGE_EMAIL)
dbsession.add(user)
dbsession.flush()
parent = DummyResource(request=request)
resource = AccountResource('a name', parent)
result = resource[token.hash]
assert isinstance(result, ChangeEmailTokenResource)
assert result.__name__ == token.hash
assert result.__parent__ == resource
assert result.model == token
def test_account_resource_getitem_not_found(dbsession): # noqa: F811
''' test '__getitem__()' method raises KeyError '''
from ordr.models.account import Role, TokenSubject
from ordr.resources.account import AccountResource
request = DummyRequest(dbsession=dbsession)
user = get_example_user(Role.NEW)
user.issue_token(request, TokenSubject.CHANGE_EMAIL)
dbsession.add(user)
dbsession.flush()
parent = DummyResource(request=request)
resource = AccountResource('a name', parent)
with pytest.raises(KeyError):
resource['unknown hash']
def test_account_resource_get_settings_form():
''' test the setup of the settings form'''
from ordr.resources.account import AccountResource
import deform
request = DummyRequest()
parent = DummyResource(request=request)
resource = AccountResource('some name', parent)
form = resource.get_settings_form()
assert isinstance(form, deform.Form)
assert len(form.buttons) == 2
assert form.buttons[0].title == 'Change Settings'
assert form.buttons[1].title == 'Cancel'
def test_account_resource_get_password_form():
''' test the setup of the change password form'''
from ordr.resources.account import AccountResource
import deform
request = DummyRequest()
parent = DummyResource(request=request)
resource = AccountResource('some name', parent)
form = resource.get_password_form()
assert isinstance(form, deform.Form)
assert len(form.buttons) == 2
assert form.buttons[0].title == 'Change Password'
assert form.buttons[1].title == 'Cancel'

9
tests/resources/root.py

@ -2,7 +2,11 @@ @@ -2,7 +2,11 @@
import pytest
from ordr.resources.account import RegistrationResource, PasswordResetResource
from ordr.resources.account import (
RegistrationResource,
PasswordResetResource,
AccountResource
)
def test_root_init():
@ -25,7 +29,8 @@ def test_root_acl(): @@ -25,7 +29,8 @@ def test_root_acl():
@pytest.mark.parametrize(
'key,resource_class', [
('register', RegistrationResource),
('forgot', PasswordResetResource)
('forgot', PasswordResetResource),
('account', AccountResource)
]
)
def test_root_getitem(key, resource_class):

20
tests/schemas/helpers.py → tests/schemas/validators.py

@ -9,7 +9,7 @@ from .. import app_config, dbsession, get_example_user # noqa: F401 @@ -9,7 +9,7 @@ from .. import app_config, dbsession, get_example_user # noqa: F401
def test_deferred_csrf_default():
''' deferred_csrf_default should return a csrf token '''
from ordr.schemas.helpers import deferred_csrf_default
from ordr.schemas.validators import deferred_csrf_default
from pyramid.csrf import get_csrf_token
request = DummyRequest()
@ -20,7 +20,7 @@ def test_deferred_csrf_default(): @@ -20,7 +20,7 @@ def test_deferred_csrf_default():
def test_deferred_csrf_validator_ok():
''' test deferred_csrf_validator with valid csrf token '''
from ordr.schemas.helpers import deferred_csrf_validator
from ordr.schemas.validators import deferred_csrf_validator
from pyramid.csrf import get_csrf_token
request = DummyRequest()
@ -34,7 +34,7 @@ def test_deferred_csrf_validator_ok(): @@ -34,7 +34,7 @@ def test_deferred_csrf_validator_ok():
@pytest.mark.parametrize('post', [{}, {'csrf_token': 'Albatross!'}])
def test_deferred_csrf_validator_fails_on_no_csrf_token(post):
''' test deferred_csrf_validator with invalid or missing csrf token '''
from ordr.schemas.helpers import deferred_csrf_validator
from ordr.schemas.validators import deferred_csrf_validator
from colander import Invalid
request = DummyRequest()
@ -47,7 +47,7 @@ def test_deferred_csrf_validator_fails_on_no_csrf_token(post): @@ -47,7 +47,7 @@ def test_deferred_csrf_validator_fails_on_no_csrf_token(post):
def test_deferred_unique_username_validator_ok(dbsession): # noqa: F811
''' unknown usernames should not raise an invalidation error '''
from ordr.schemas.helpers import deferred_unique_username_validator
from ordr.schemas.validators import deferred_unique_username_validator
from ordr.models.account import Role
request = DummyRequest(dbsession=dbsession)
@ -63,7 +63,7 @@ def test_deferred_unique_username_validator_ok(dbsession): # noqa: F811 @@ -63,7 +63,7 @@ def test_deferred_unique_username_validator_ok(dbsession): # noqa: F811
def test_deferred_unique_username_validator_fails(dbsession): # noqa: F811
''' known username should raise an invalidation error '''
from ordr.schemas.helpers import deferred_unique_username_validator
from ordr.schemas.validators import deferred_unique_username_validator
from ordr.models.account import Role
from colander import Invalid
@ -81,7 +81,7 @@ def test_deferred_unique_username_validator_fails(dbsession): # noqa: F811 @@ -81,7 +81,7 @@ def test_deferred_unique_username_validator_fails(dbsession): # noqa: F811
def test_deferred_unique_email_validator_ok(dbsession): # noqa: F811
''' unknown emails should not raise an invalidation error '''
from ordr.schemas.helpers import deferred_unique_email_validator
from ordr.schemas.validators import deferred_unique_email_validator
from ordr.models.account import Role
context = DummyResource(model=None)
@ -102,7 +102,7 @@ def test_deferred_unique_email_validator_ok_same_user(dbsession): # noqa: F811 @@ -102,7 +102,7 @@ def test_deferred_unique_email_validator_ok_same_user(dbsession): # noqa: F811
if a user is edited and the mail address is not change, no invalidation
error should be raised
'''
from ordr.schemas.helpers import deferred_unique_email_validator
from ordr.schemas.validators import deferred_unique_email_validator
from ordr.models.account import Role
user = get_example_user(Role.USER)
@ -122,7 +122,7 @@ def test_deferred_unique_email_validator_ok_same_user(dbsession): # noqa: F811 @@ -122,7 +122,7 @@ def test_deferred_unique_email_validator_ok_same_user(dbsession): # noqa: F811
)
def test_deferred_unique_email_validator_fails(dbsession, email):
''' known, empty or malformed emails should raise an invalidation error '''
from ordr.schemas.helpers import deferred_unique_email_validator
from ordr.schemas.validators import deferred_unique_email_validator
from ordr.models.account import Role
from colander import Invalid
@ -141,7 +141,7 @@ def test_deferred_unique_email_validator_fails(dbsession, email): @@ -141,7 +141,7 @@ def test_deferred_unique_email_validator_fails(dbsession, email):
def test_deferred_password_validator_ok():
''' correct password should not raise invalidation error '''
from ordr.schemas.helpers import deferred_password_validator
from ordr.schemas.validators import deferred_password_validator
from ordr.models.account import Role
user = get_example_user(Role.USER)
@ -153,7 +153,7 @@ def test_deferred_password_validator_ok(): @@ -153,7 +153,7 @@ def test_deferred_password_validator_ok():
def test_deferred_password_validator_fails():
''' incorrect password should raise invalidation error '''
from ordr.schemas.helpers import deferred_password_validator
from ordr.schemas.validators import deferred_password_validator
from ordr.models.account import Role
from colander import Invalid

311
tests/views/account.py

@ -0,0 +1,311 @@ @@ -0,0 +1,311 @@
import deform
from pyramid.httpexceptions import HTTPFound
from pyramid.testing import DummyRequest, DummyResource
from .. import ( # noqa: F401
app_config,
dbsession,
get_example_user,
get_post_request
)
def test_account_redirect():
''' redirect on root of account resource '''
from ordr.views.account import account
request = DummyRequest()
result = account(None, request)
assert isinstance(result, HTTPFound)
assert result.location == 'http://example.com//'
def test_settings_form():
''' tests for displaying the settings form '''
from ordr.models.account import Role
from ordr.resources.account import AccountResource
from ordr.schemas.account import SettingsSchema
from ordr.views.account import settings_form
user = get_example_user(Role.USER)
request = DummyRequest(user=user)
parent = DummyResource(request=request)
context = AccountResource(None, parent)
result = settings_form(context, request)
form = result['form']
assert isinstance(form, deform.Form)
assert isinstance(form.schema, SettingsSchema)
def test_settings_form_processing_valid_data(dbsession): # noqa: F811
''' tests for processing the settings form
The data is valid, but no email change requested
'''
from ordr.models.account import Role, Token, User
from ordr.resources.account import AccountResource
from ordr.views.account import settings_form_processing
data = {
'username': 'TerryG',
'first_name': 'Amy',
'last_name': 'McDonald',
'email': 'gilliam@example.com',
'confirmation': 'Terry',
'change': 'Change Settings'
}
user = get_example_user(Role.USER)
dbsession.add(user)
dbsession.flush()
request = get_post_request(data=data, dbsession=dbsession, user=user)
parent = DummyResource(request=request)
context = AccountResource(None, parent)
request.context = context
result = settings_form_processing(context, request)
assert isinstance(result, HTTPFound)
assert result.location == 'http://example.com//'
account = dbsession.query(User).first()
assert account.username == 'TerryGilliam'
assert account.first_name == 'Amy'
assert account.last_name == 'McDonald'
assert account.email == 'gilliam@example.com'
assert dbsession.query(Token).count() == 0
def test_settings_form_processing_mail_change(dbsession): # noqa: F811
''' tests for processing the settings form
The data is valid and an email change is requested
'''
from ordr.models.account import Role, Token, TokenSubject, User
from ordr.resources.account import AccountResource
from ordr.views.account import settings_form_processing
data = {
'username': 'TerryG',
'first_name': 'Amy',
'last_name': 'McDonald',
'email': 'amy@example.com',
'confirmation': 'Terry',
'change': 'Change Settings'
}
user = get_example_user(Role.USER)
dbsession.add(user)
request = get_post_request(data=data, dbsession=dbsession, user=user)
parent = DummyResource(request=request)
context = AccountResource(None, parent)
request.context = context
result = settings_form_processing(context, request)
assert isinstance(result, HTTPFound)
assert result.location == 'http://example.com//verify'
account = dbsession.query(User).first()
assert account.username == 'TerryGilliam'
assert account.first_name == 'Amy'
assert account.last_name == 'McDonald'
assert account.email == 'gilliam@example.com'
token = dbsession.query(Token).first()
assert token.subject == TokenSubject.CHANGE_EMAIL
assert token.payload == {'email': 'amy@example.com'}
# a verification email should be sent
# this is tested in the functional test since request.registry.notify
# doesn't know about event subscribers in the unittest
def test_settings_form_processing_invalid_data(dbsession): # noqa: F811
''' tests for processing the settings form with invalid data '''
from ordr.models.account import Role
from ordr.resources.account import AccountResource
from ordr.schemas.account import SettingsSchema
from ordr.views.account import settings_form_processing
data = {
'username': 'TerryG',
'first_name': 'Amy',
'last_name': 'McDonald',
'email': 'this is not an email address',
'confirmation': 'Terry',
'change': 'Change Settings'
}
user = get_example_user(Role.USER)
dbsession.add(user)
request = get_post_request(data=data, dbsession=dbsession, user=user)
parent = DummyResource(request=request)
context = AccountResource(None, parent)
request.context = context
result = settings_form_processing(context, request)
form = result['form']
assert isinstance(form, deform.Form)
assert isinstance(form.schema, SettingsSchema)
def test_settings_form_processing_cancel(dbsession): # noqa: F811
''' tests for processing the settings form with invalid data '''
from ordr.models.account import Role, User
from ordr.resources.account import AccountResource
from ordr.views.account import settings_form_processing
data = {
'username': 'TerryG',
'first_name': 'Amy',
'last_name': 'McDonald',
'email': 'this is not an email address',
'confirmation': 'Terry',
'cancel': 'cancel'
}
user = get_example_user(Role.USER)
dbsession.add(user)
request = get_post_request(data=data, dbsession=dbsession, user=user)
parent = DummyResource(request=request)
context = AccountResource(None, parent)
request.context = context
result = settings_form_processing(context, request)
assert isinstance(result, HTTPFound)
assert result.location == 'http://example.com//'
account = dbsession.query(User).first()
assert account.first_name == 'Terry'
def test_verify_email(dbsession): # noqa: F811
''' tests for processing the change password form '''
from ordr.models.account import Role, Token, TokenSubject
from ordr.views.account import verify_email
user = get_example_user(Role.USER)
request = DummyRequest(dbsession=dbsession, user=user)
user.issue_token(
request,
TokenSubject.CHANGE_EMAIL,
{'email': 'amy@example.com'}
)
dbsession.add(user)
dbsession.flush()
token = dbsession.query(Token).first()
context = DummyResource(model=token)
result = verify_email(context, request)
assert result == {}
assert user.email == 'amy@example.com'
assert dbsession.query(Token).count() == 0
def test_password_form():
''' tests for displaying the change password form '''
from ordr.models.account import Role
from ordr.resources.account import AccountResource
from ordr.schemas.account import ChangePasswordSchema
from ordr.views.account import password_form
user = get_example_user(Role.USER)
request = DummyRequest(user=user)
parent = DummyResource(request=request)
context = AccountResource(None, parent)
result = password_form(context, request)
form = result['form']
assert isinstance(form, deform.Form)
assert isinstance(form.schema, ChangePasswordSchema)
def test_password_form_processing_valid(dbsession): # noqa: F811
''' tests for processing the change password form '''
from ordr.models.account import Role
from ordr.resources.account import AccountResource
from ordr.views.account import password_form_processing
data = {
'__start__': 'password:mapping',
'password': 'Make Amy McDonald A Rich Girl Fund',
'password-confirm': 'Make Amy McDonald A Rich Girl Fund',
'__end__': 'password:mapping',
'confirmation': 'Terry',
'change': 'Change Password'
}
user = get_example_user(Role.USER)
request = get_post_request(data=data, user=user)
parent = DummyResource(request=request)
context = AccountResource(None, parent)
result = password_form_processing(context, request)
assert isinstance(result, HTTPFound)
assert result.location == 'http://example.com//changed'
assert not user.check_password('Terry')
assert user.check_password('Make Amy McDonald A Rich Girl Fund')
def test_password_form_processing_invalid(dbsession): # noqa: F811
''' tests for processing the change password form '''
from ordr.models.account import Role
from ordr.resources.account import AccountResource
from ordr.schemas.account import ChangePasswordSchema
from ordr.views.account import password_form_processing
data = {
'__start__': 'password:mapping',
'password': 'Make Amy McDonald A Rich Girl Fund',
'password-confirm': 'Make Amy McDonald A Rich Girl Fund',
'__end__': 'password:mapping',
'confirmation': 'not the right password for confirmation',
'change': 'Change Password'
}
user = get_example_user(Role.USER)
request = get_post_request(data=data, user=user)
parent = DummyResource(request=request)
context = AccountResource(None, parent)
result = password_form_processing(context, request)
form = result['form']
assert isinstance(form, deform.Form)
assert isinstance(form.schema, ChangePasswordSchema)
assert user.check_password('Terry')
def test_password_form_processing_cancel(dbsession): # noqa: F811
''' tests canceling the change password form '''
from ordr.models.account import Role
from ordr.resources.account import AccountResource
from ordr.views.account import password_form_processing
data = {
'__start__': 'password:mapping',
'password': 'Make Amy McDonald A Rich Girl Fund',
'password-confirm': 'Make Amy McDonald A Rich Girl Fund',
'__end__': 'password:mapping',
'confirmation': 'Terry',
'cancel': 'cancel'
}
user = get_example_user(Role.USER)
request = get_post_request(data=data, user=user)
parent = DummyResource(request=request)
context = AccountResource(None, parent)
result = password_form_processing(context, request)
assert isinstance(result, HTTPFound)
assert result.location == 'http://example.com//'
assert user.check_password('Terry')
def test_password_changed():
''' show password has changed message '''
from ordr.views.account import password_changed
result = password_changed(None, None)
assert result == {}

6
tests/views/forgotten_password.py

@ -156,7 +156,7 @@ def test_reset_password_form_processing_valid(dbsession): # noqa: F811 @@ -156,7 +156,7 @@ def test_reset_password_form_processing_valid(dbsession): # noqa: F811
'__end__': 'password:mapping',
'change': 'Set New Password'
}
request = get_post_request(dbsession, data)
request = get_post_request(data, dbsession=dbsession)
user = get_example_user(Role.USER)
dbsession.add(user)
@ -194,7 +194,7 @@ def test_reset_password_form_processing_invalid_data(dbsession): # noqa: F811 @@ -194,7 +194,7 @@ def test_reset_password_form_processing_invalid_data(dbsession): # noqa: F811
'__end__': 'password:mapping',
'change': 'Set New Password'
}
request = get_post_request(dbsession, data)
request = get_post_request(data, dbsession=dbsession)
user = get_example_user(Role.USER)
dbsession.add(user)
@ -225,7 +225,7 @@ def test_reset_password_form_processing_cancel(dbsession): # noqa: F811 @@ -225,7 +225,7 @@ def test_reset_password_form_processing_cancel(dbsession): # noqa: F811
'__end__': 'password:mapping',
'cancel': 'Cancel'
}
request = get_post_request(dbsession, data)
request = get_post_request(data, dbsession=dbsession)
user = get_example_user(Role.USER)
dbsession.add(user)

6
tests/views/registration.py

@ -47,7 +47,7 @@ def test_registration_form_valid(dbsession): # noqa: F811 @@ -47,7 +47,7 @@ def test_registration_form_valid(dbsession): # noqa: F811
from ordr.views.registration import registration_form_processing
data = REGISTRATION_FORM_DATA.copy()
request = get_post_request(dbsession, data)
request = get_post_request(data, dbsession=dbsession)
parent = DummyResource(request=request)
context = RegistrationResource(name=None, parent=parent)
result = registration_form_processing(context, request)
@ -81,7 +81,7 @@ def test_registration_form_invalid(dbsession): # noqa: F811 @@ -81,7 +81,7 @@ def test_registration_form_invalid(dbsession): # noqa: F811
data = REGISTRATION_FORM_DATA.copy()
data['email'] = 'not an email address'
request = get_post_request(dbsession, data)
request = get_post_request(data, dbsession=dbsession)
parent = DummyResource(request=request)
context = RegistrationResource(name=None, parent=parent)
result = registration_form_processing(context, request)
@ -96,7 +96,7 @@ def test_registration_form_no_create_button(dbsession): # noqa: F811 @@ -96,7 +96,7 @@ def test_registration_form_no_create_button(dbsession): # noqa: F811
data = REGISTRATION_FORM_DATA.copy()
data.pop('create')
request = get_post_request(dbsession, data)
request = get_post_request(data, dbsession=dbsession)
parent = DummyResource(request=request)
context = RegistrationResource(name=None, parent=parent)
result = registration_form_processing(context, request)