Browse Source

finished registration workflow

rework
Holger Frey 7 years ago
parent
commit
1c96f9b970
  1. 4
      ordr/events.py
  2. 29
      ordr/models/account.py
  3. 2
      ordr/resources/__init__.py
  4. 22
      ordr/resources/account.py
  5. 7
      ordr/resources/helpers.py
  6. 6
      ordr/schemas/account.py
  7. 2
      ordr/security.py
  8. 36
      ordr/templates/account/registration_completed.jinja2
  9. 4
      ordr/templates/account/registration_form.jinja2
  10. 35
      ordr/templates/account/registration_verify.jinja2
  11. 4
      ordr/templates/layout.jinja2
  12. 2
      ordr/views/pages.py
  13. 28
      ordr/views/registration.py
  14. 5
      tests/__init__.py
  15. 27
      tests/_functional/__init__.py
  16. 4
      tests/_functional/errors.py
  17. 20
      tests/_functional/layout.py
  18. 34
      tests/_functional/login_logout.py
  19. 14
      tests/_functional/pages.py
  20. 44
      tests/_functional/registration.py
  21. 14
      tests/models/account.py
  22. 91
      tests/resources/account.py
  23. 44
      tests/resources/base_child_resource.py
  24. 2
      tests/resources/root.py
  25. 2
      tests/schemas/__init__.py
  26. 16
      tests/security.py
  27. 2
      tests/views/errors.py
  28. 10
      tests/views/pages.py
  29. 52
      tests/views/registration.py

4
ordr/events.py

@ -30,24 +30,28 @@ class UserNotification(object): @@ -30,24 +30,28 @@ class UserNotification(object):
class ActivationNotification(UserNotification):
''' user notification for account activation '''
subject = '[ordr] Your account was activated'
template = 'ordr:templates/emails/activation.jinja2'
class OrderStatusNotification(UserNotification):
''' user notification for order status change '''
subject = '[ordr] Order Status Change'
template = 'ordr:templates/emails/order.jinja2'
class PasswordResetNotification(UserNotification):
''' user notification for password reset link '''
subject = '[ordr] Password Reset'
template = 'ordr:templates/emails/password_reset.jinja2'
class RegistrationNotification(UserNotification):
''' user notification for account activation '''
subject = '[ordr] Please verify your email address'
template = 'ordr:templates/emails/registration.jinja2'

29
ordr/models/account.py

@ -194,3 +194,32 @@ class Token(Base): @@ -194,3 +194,32 @@ class Token(Base):
owner=owner,
expires=expires
)
@classmethod
def retrieve(cls, request, hash, subject=None):
''' returns a token from the database
The database is queried for a token with the given hash. If an
optional subject is given, the query will search only for tokens of
this kind.
The method will return None if a token could not be found or the
token has already expired. If the token has expired, it will be deleted
from the database
:param pyramid.request.Request request: the current request object
:param str hash: token hash
:param ordr2.models.account.TokenSubject subject: kind of token
:rtype: ordr2.models.account.Token or None
'''
query = request.dbsession.query(cls).filter_by(hash=hash)
if subject:
query = query.filter_by(subject=subject)
token = query.first()
if token is None:
return None
elif token.expires < datetime.utcnow():
request.dbsession.delete(token)
return None
return token

2
ordr/resources/__init__.py

@ -37,7 +37,7 @@ class RootResource: @@ -37,7 +37,7 @@ class RootResource:
'register': RegistrationResource
}
child_class = map[key]
return child_class(request=self.request, name=key, parent=self)
return child_class(name=key, parent=self)
def includeme(config):

22
ordr/resources/account.py

@ -3,11 +3,26 @@ @@ -3,11 +3,26 @@
import deform
from pyramid.security import Allow, Everyone, DENY_ALL
from ordr.models.account import Token, TokenSubject
from ordr.schemas.account import RegistrationSchema
from .helpers import BaseChildResource
class RegistrationTokenResource(BaseChildResource):
''' Resource for vaildating a new registered user's email
:param pyramid.request.Request request: the current request object
:param str name: the name of the resource
:param parent: the parent resouce
'''
def __acl__(self):
''' access controll list for the resource '''
return [(Allow, Everyone, 'view'), DENY_ALL]
class RegistrationResource(BaseChildResource):
''' The resource for new user registration
@ -22,6 +37,13 @@ class RegistrationResource(BaseChildResource): @@ -22,6 +37,13 @@ class RegistrationResource(BaseChildResource):
''' access controll list for the resource '''
return [(Allow, Everyone, 'view'), DENY_ALL]
def __getitem__(self, key):
''' returns a resource for a valid registration token '''
token = Token.retrieve(self.request, key, TokenSubject.REGISTRATION)
if token is None:
raise KeyError(f'Token {key} not found')
return RegistrationTokenResource(name=key, parent=self, model=token)
def get_registration_form(self, **kwargs):
''' returns the registration form'''
settings = {

7
ordr/resources/helpers.py

@ -3,16 +3,17 @@ @@ -3,16 +3,17 @@
class BaseChildResource:
def __init__(self, request, name, parent):
def __init__(self, name, parent, model=None):
''' Create a child resource
:param pyramid.request.Request request: the current request object
:param str name: the name of the resource
:param parent: the parent resouce
:param model: optional data model for the resource
'''
self.request = request
self.__name__ = name
self.__parent__ = parent
self.request = parent.request
self.model = model
def __acl__(self):
''' access controll list for the resource '''

6
ordr/schemas/account.py

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
import colander
import deform
from . import CSRFSchema
from .helpers import (
deferred_unique_email_validator,
deferred_unique_username_validator,
@ -23,18 +23,22 @@ class RegistrationSchema(CSRFSchema): @@ -23,18 +23,22 @@ class RegistrationSchema(CSRFSchema):
validator=deferred_unique_username_validator,
oid='registration_username'
)
first_name = colander.SchemaNode(
colander.String(),
oid='registration_first_name'
)
last_name = colander.SchemaNode(
colander.String(),
oid='registration_last_name'
)
email = colander.SchemaNode(
colander.String(),
validator=deferred_unique_email_validator
)
password = colander.SchemaNode(
colander.String(),
widget=deform.widget.CheckedPasswordWidget(),

2
ordr/security.py

@ -9,6 +9,8 @@ from pyramid.settings import aslist @@ -9,6 +9,8 @@ from pyramid.settings import aslist
from ordr.models.account import User
#: passlib context for hashing passwords
# at least one scheme must be set in advance, will be overridden by the
# settings in the .ini file.
password_context = CryptContext(schemes=['argon2'])

36
ordr/templates/account/registration_completed.jinja2

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
{% extends "ordr:templates/layout.jinja2" %}
{% block title %} Ordr | Registration {% endblock title %}
{% block content %}
<div class="row justify-content-md-center mt-3">
<div class="col-6">
<h1>Registration</h1>
</div>
</div>
<div class="row justify-content-md-center mt-3">
<div class="col-2">
<p class="text-secondary">
Step 1: Registration
</p>
</div>
<div class="col-2">
<p class="text-secondary">
Step 2: Validate Email
</p>
</div>
<div class="col-2">
<p class="text-primary">
Step 3: Finished
</p>
</div>
</div>
<div class="row justify-content-md-center mt-3">
<div class="col-6">
<h3>Registration Completed</h3>
<p class="mt-3">Thank you for verifying your email address.</p>
<p>Before you can start ordering, an administrator must activate your account</p>
<p>You'll receive an email when your account is activated</p>
</div>
</div>
{% endblock content %}

4
ordr/templates/account/registration_form.jinja2

@ -1,9 +1,11 @@ @@ -1,9 +1,11 @@
{% extends "ordr:templates/layout.jinja2" %}
{% block title %} Ordr | Registration {% endblock title %}
{% block content %}
<div class="row justify-content-md-center mt-3">
<div class="col-6">
<h2>Registration</h2>
<h1>Registration</h1>
</div>
</div>
<div class="row justify-content-md-center mt-3">

35
ordr/templates/account/registration_verify.jinja2

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
{% extends "ordr:templates/layout.jinja2" %}
{% block title %} Ordr | Registration {% endblock title %}
{% block content %}
<div class="row justify-content-md-center mt-3">
<div class="col-6">
<h1>Registration</h1>
</div>
</div>
<div class="row justify-content-md-center mt-3">
<div class="col-2">
<p class="text-secondary">
Step 1: Registration
</p>
</div>
<div class="col-2">
<p class="text-primary">
Step 2: Validate Email
</p>
</div>
<div class="col-2">
<p class="text-secondary">
Step 3: Finished
</p>
</div>
</div>
<div class="row justify-content-md-center mt-3">
<div class="col-6">
<h3>Verify Your Email Address</h3>
<p class="mt-3">To complete the registration process an email has been sent to you.</p>
<p>Please follow the link in the email to verify your address and complete the registration process.</p>
</div>
</div>
{% endblock content %}

4
ordr/templates/layout.jinja2

@ -8,9 +8,7 @@ @@ -8,9 +8,7 @@
<meta name="author" content="IMTEk / CPI / Holger Frey">
<link rel="shortcut icon" href="{{request.static_url('ordr:static/pyramid-16x16.png')}}">
{% block title %}
<title>Ordr</title>
{% endblock title %}
<title>{% block title %} Ordr {% endblock title %}</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">

2
ordr/views/pages.py

@ -52,9 +52,11 @@ def check_login(context, request): @@ -52,9 +52,11 @@ def check_login(context, request):
.filter_by(username=username)
.first()
)
if user and user.is_active and user.check_password(password):
headers = remember(request, user.id)
return HTTPFound(request.resource_url(request.root), headers=headers)
return {'loginerror': True}

28
ordr/views/registration.py

@ -32,6 +32,7 @@ def registration_form_processing(context, request): @@ -32,6 +32,7 @@ def registration_form_processing(context, request):
''' process registration form '''
if 'create' not in request.POST:
return HTTPFound(request.resource_url(request.root))
form = context.get_registration_form()
data = request.POST.items()
try:
@ -56,3 +57,30 @@ def registration_form_processing(context, request): @@ -56,3 +57,30 @@ def registration_form_processing(context, request):
request.registry.notify(notification)
return HTTPFound(request.resource_url(context, 'verify'))
@view_config(
context='ordr.resources.account.RegistrationResource',
name='verify',
permission='view',
request_method='GET',
renderer='ordr:templates/account/registration_verify.jinja2'
)
def verify(context, request):
''' show email verification text '''
return {}
@view_config(
context='ordr.resources.account.RegistrationTokenResource',
permission='view',
request_method='GET',
renderer='ordr:templates/account/registration_completed.jinja2'
)
def completed(context, request):
''' show email verification text '''
token = context.model
account = token.owner
account.role = Role.NEW
request.dbsession.delete(token)
return {}

5
tests/__init__.py

@ -64,6 +64,7 @@ def dbsession(app_config): @@ -64,6 +64,7 @@ def dbsession(app_config):
def get_example_user(role):
''' get the user model for one well known user '''
from ordr.models import User
id_, first_name, last_name = EXAMPLE_USER_DATA[role.name]
user = User(
id=id_,
@ -74,11 +75,15 @@ def get_example_user(role): @@ -74,11 +75,15 @@ def get_example_user(role):
role=role
)
user.set_password(first_name)
return user
def get_post_request(dbsession, data):
''' 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)

27
tests/_functional/__init__.py

@ -1,13 +1,19 @@ @@ -1,13 +1,19 @@
''' functional tests for ordr2 '''
import pytest
import re
import transaction
import webtest
from bs4 import BeautifulSoup
from .. import APP_SETTINGS, get_example_user
WEBTEST_SETTINGS = APP_SETTINGS.copy()
# WEBTEST_SETTINGS['pyramid.includes'].append('pyramid_mailer.testing')
WEBTEST_SETTINGS['pyramid.includes'] = [
'pyramid_mailer.testing'
]
class CustomTestApp(webtest.TestApp):
@ -15,7 +21,7 @@ class CustomTestApp(webtest.TestApp): @@ -15,7 +21,7 @@ class CustomTestApp(webtest.TestApp):
pass
def login(self, username, password):
''' stub for user login '''
''' login '''
self.logout()
result = self.get('/login')
login_form = result.forms[0]
@ -24,7 +30,7 @@ class CustomTestApp(webtest.TestApp): @@ -24,7 +30,7 @@ class CustomTestApp(webtest.TestApp):
login_form.submit()
def logout(self):
''' stub for user logout '''
''' logout '''
self.get('/logout')
def reset(self):
@ -41,9 +47,21 @@ def create_users(dbsession): @@ -41,9 +47,21 @@ def create_users(dbsession):
dbsession.add(user)
def get_token_url(email, prefix='/'):
''' extracts an account token url from an email '''
soup = BeautifulSoup(email.html, 'html.parser')
for link in soup.find_all('a'):
if re.search(prefix + '[a-f0-9]{32}', link['href']):
return link['href']
@pytest.fixture(scope='module')
def testappsetup():
''' fixture for using webtest '''
''' fixture for using webtest
this fixture just sets up the testapp. please use the testapp() fixture
below for real tests.
'''
from ordr.models.meta import Base
from ordr.models import get_tm_session
from ordr import main
@ -67,5 +85,6 @@ def testappsetup(): @@ -67,5 +85,6 @@ def testappsetup():
@pytest.fixture(scope='function')
def testapp(testappsetup):
''' fixture using webtests, resets the logged every time '''
testappsetup.reset()
yield testappsetup

4
tests/_functional/errors.py

@ -4,5 +4,5 @@ from . import testappsetup, testapp # noqa: F401 @@ -4,5 +4,5 @@ from . import testappsetup, testapp # noqa: F401
def test_404(testapp): # noqa: F811
result = testapp.get('/unknown', status=404)
assert '404' in result
response = testapp.get('/unknown', status=404)
assert '404' in response

20
tests/_functional/layout.py

@ -10,13 +10,14 @@ from . import testappsetup, testapp # noqa: F401 @@ -10,13 +10,14 @@ from . import testappsetup, testapp # noqa: F401
def test_navbar_no_user(testapp): # noqa: F811
result = testapp.get('/faq')
navbar = result.html.find('nav', class_='navbar-dark')
response = testapp.get('/faq')
navbar = response.html.find('nav', class_='navbar-dark')
expected = ['/', '/', '/faq', '/register']
hrefs = [a['href'] for a in navbar.find_all('a')]
assert expected == hrefs
assert '/orders' not in result
assert 'nav-item dropdown' not in result
assert '/orders' not in response
assert 'nav-item dropdown' not in response
@pytest.mark.parametrize( # noqa: F811
@ -28,14 +29,13 @@ def test_navbar_no_user(testapp): # noqa: F811 @@ -28,14 +29,13 @@ def test_navbar_no_user(testapp): # noqa: F811
)
def test_navbar_with_user(testapp, username, password, extras):
testapp.login(username, password)
result = testapp.get('/faq')
navbar = result.html.find('nav', class_='navbar-dark')
response = testapp.get('/faq')
navbar = response.html.find('nav', class_='navbar-dark')
hrefs = [a['href'] for a in navbar.find_all('a')]
expected = ['/', '/orders', '/faq']
expected.extend(extras)
expected.extend(['#', '/logout', '/account'])
print('expected', expected)
print('found ', hrefs)
assert expected == hrefs
assert 'nav-item dropdown' in result
assert username in result
assert 'nav-item dropdown' in response
assert username in response

34
tests/_functional/login_logout.py

@ -6,27 +6,33 @@ from . import testappsetup, testapp # noqa: F401 @@ -6,27 +6,33 @@ from . import testappsetup, testapp # noqa: F401
def test_login_get(testapp): # noqa: F811
result = testapp.get('/login')
active = result.html.find('li', class_='active')
response = testapp.get('/login')
active = response.html.find('li', class_='active')
assert active.a['href'] == '/'
expected = {'/', '/faq', '/register', '/forgot', '/register'}
hrefs = {a['href'] for a in result.html.find_all('a')}
hrefs = {a['href'] for a in response.html.find_all('a')}
assert expected == hrefs
forms = result.html.find_all('form')
forms = response.html.find_all('form')
assert len(forms) == 1
login_form = forms[0]
assert login_form['action'] == '/login'
assert login_form['method'] == 'POST'
assert 'account is not activated' not in result
assert 'account is not activated' not in response
def test_login_ok(testapp): # noqa: F811
result = testapp.get('/login')
login_form = result.forms[0]
response = testapp.get('/login')
login_form = response.forms[0]
login_form['username'] = 'TerryGilliam'
login_form['password'] = 'Terry'
result = login_form.submit()
assert result.location == 'http://localhost/'
response = login_form.submit()
assert response.location == 'http://localhost/'
@pytest.mark.parametrize( # noqa: F811
@ -34,9 +40,11 @@ def test_login_ok(testapp): # noqa: F811 @@ -34,9 +40,11 @@ def test_login_ok(testapp): # noqa: F811
[('John', 'Cleese'), ('unknown user', 'wrong password')]
)
def test_login_denied(testapp, username, password):
result = testapp.get('/login')
login_form = result.forms[0]
response = testapp.get('/login')
login_form = response.forms[0]
login_form['username'] = 'John'
login_form['password'] = 'Cleese'
result = login_form.submit()
assert 'account is not activated' in result
response = login_form.submit()
assert 'account is not activated' in response

14
tests/_functional/pages.py

@ -4,14 +4,16 @@ from . import testappsetup, testapp # noqa: F401 @@ -4,14 +4,16 @@ from . import testappsetup, testapp # noqa: F401
def test_welcome(testapp): # noqa: F811
result = testapp.get('/')
assert result.location == 'http://localhost/login'
response = testapp.get('/')
assert response.location == 'http://localhost/login'
testapp.login('TerryGilliam', 'Terry')
result = testapp.get('/')
assert result.location == 'http://localhost/orders'
response = testapp.get('/')
assert response.location == 'http://localhost/orders'
def test_faq(testapp): # noqa: F811
result = testapp.get('/faq')
active = result.html.find('li', class_='active')
response = testapp.get('/faq')
active = response.html.find('li', class_='active')
assert active.a['href'] == '/faq'

44
tests/_functional/registration.py

@ -1,9 +1,47 @@ @@ -1,9 +1,47 @@
''' functional tests for ordr2.views.registration '''
from . import testappsetup, testapp # noqa: F401
from pyramid_mailer import get_mailer
from . import testappsetup, testapp, get_token_url # noqa: F401
def test_registration_form(testapp): # noqa: F811
result = testapp.get('/register')
active = result.html.find('li', class_='active')
response = testapp.get('/register')
active = response.html.find('li', class_='active')
assert active.a['href'] == '/register'
def test_registration_form_invalid(testapp): # noqa: F811
response = testapp.get('/register')
form = response.form
form['email'] = 'not an email address'
response = form.submit(name='create')
assert 'Invalid email address' in response
def test_registration_process(testapp): # noqa: F811
response = testapp.get('/register')
form = response.form
form['username'] = 'AmyMcDonald',
form['first_name'] = 'Amy',
form['last_name'] = 'Mc Donald',
form['email'] = 'amy.mcdonald@example.com',
form['password'] = 'Make Amy McDonald A Rich Girl Fund',
form['password-confirm'] = 'Make Amy McDonald A Rich Girl Fund',
response = form.submit(name='create')
assert response.location == 'http://localhost/register/verify'
response = response.follow()
assert 'Please follow the link in the email' in response
# click the email verification token
mailer = get_mailer(testapp.app.registry)
email = mailer.outbox[-1]
assert email.subject == '[ordr] Please verify your email address'
token_link = get_token_url(email, prefix='/register/')
response = testapp.get(token_link)
assert 'Registration Completed' in response

14
tests/models/account.py

@ -43,9 +43,11 @@ def test_user_principal(id_): @@ -43,9 +43,11 @@ def test_user_principal(id_):
)
def test_user_principals(name, principals):
from ordr.models.account import User, Role
user = User(id=1, role=Role[name])
expected = ['user:1']
expected.extend(principals)
assert expected == user.principals
@ -68,9 +70,11 @@ def test_user_is_active(name, expected): @@ -68,9 +70,11 @@ def test_user_is_active(name, expected):
def test_user_set_password():
from ordr.models.account import User
from ordr.security import password_context
password_context.update(schemes=['argon2'])
user = User()
assert user.password_hash is None
user.set_password('password')
assert user.password_hash.startswith('$argon2')
@ -85,17 +89,20 @@ def test_user_set_password(): @@ -85,17 +89,20 @@ def test_user_set_password():
def test_user_check_password(password, expected):
from ordr.models.account import User
from ordr.security import password_context
password_context.update(schemes=['argon2'])
hash = ('$argon2i$v=19$m=512,t=2,p=2$'
'YcyZMyak9D7nvFfKmVOq1Q$fnzNh58HWfvxHvRDGjhTqA'
)
user = User(password_hash=hash)
assert user.check_password(password) == expected
def test_user_check_password_updates_old_sheme():
from ordr.models.account import User
from ordr.security import password_context
password_context.update(
schemes=['argon2', 'bcrypt'],
default='argon2',
@ -103,6 +110,7 @@ def test_user_check_password_updates_old_sheme(): @@ -103,6 +110,7 @@ def test_user_check_password_updates_old_sheme():
)
old_hash = '$2b$12$6ljSfpLaXBeEVOeaP1scUe6IAa0cztM.UBbjc1PdrI4j0vwgoYgpi'
user = User(password_hash=old_hash)
assert user.check_password('password')
assert user.password_hash.startswith('$argon2')
assert user.check_password('password')
@ -116,9 +124,11 @@ def test_user__str__(): @@ -116,9 +124,11 @@ def test_user__str__():
def test_user_issue_token(app_config): # noqa: F811
from ordr.models.account import User, Token, TokenSubject
request = DummyRequest()
user = User()
token = user.issue_token(request, TokenSubject.REGISTRATION, {'foo': 1})
assert isinstance(token, Token)
assert token.hash is not None
assert token.subject == TokenSubject.REGISTRATION
@ -128,10 +138,12 @@ def test_user_issue_token(app_config): # noqa: F811 @@ -128,10 +138,12 @@ def test_user_issue_token(app_config): # noqa: F811
def test_token_issue_token(app_config): # noqa: F811
from ordr.models.account import User, Token, TokenSubject
request = DummyRequest()
user = User()
token = Token.issue(request, user, TokenSubject.REGISTRATION, {'foo': 1})
expected_expires = datetime.utcnow() + timedelta(minutes=5)
assert isinstance(token, Token)
assert token.hash is not None
assert token.subject == TokenSubject.REGISTRATION
@ -148,12 +160,14 @@ def test_token_issue_token(app_config): # noqa: F811 @@ -148,12 +160,14 @@ def test_token_issue_token(app_config): # noqa: F811
)
def test_token_issue_token_time_from_settings(app_config, subject, delta):
from ordr.models.account import User, Token, TokenSubject
request = DummyRequest()
request.registry.settings['token_expiry.reset_password'] = 10
user = User()
token_subject = TokenSubject[subject]
token = Token.issue(request, user, token_subject, None)
expected_expires = datetime.utcnow() + timedelta(minutes=delta)
assert token.expires.timestamp() == pytest.approx(
expected_expires.timestamp(),
abs=1

91
tests/resources/account.py

@ -1,12 +1,30 @@ @@ -1,12 +1,30 @@
''' Tests for the account resources '''
from pyramid.testing import DummyRequest
import pytest
from datetime import datetime, timedelta
from pyramid.testing import DummyRequest, DummyResource
from .. import app_config, dbsession, get_example_user # noqa: F401
def test_registration_token_acl():
from pyramid.security import Allow, Everyone, DENY_ALL
from ordr.resources.account import RegistrationTokenResource
parent = DummyResource(request='request')
resource = RegistrationTokenResource('name', parent)
assert resource.__acl__() == [(Allow, Everyone, 'view'), DENY_ALL]
def test_registration_acl():
from pyramid.security import Allow, Everyone, DENY_ALL
from ordr.resources.account import RegistrationResource
resource = RegistrationResource('some request', 'a name', 'the parent')
parent = DummyResource(request='request')
resource = RegistrationResource('a name', parent)
assert resource.__acl__() == [(Allow, Everyone, 'view'), DENY_ALL]
@ -14,10 +32,77 @@ def test_registration_get_registration_form(): @@ -14,10 +32,77 @@ def test_registration_get_registration_form():
from pyramid.security import Allow, Everyone, DENY_ALL
from ordr.resources.account import RegistrationResource
import deform
request = DummyRequest()
resource = RegistrationResource(request, 'a name', 'the parent')
parent = DummyResource(request=request)
resource = RegistrationResource('a name', parent)
form = resource.get_registration_form()
assert isinstance(form, deform.Form)
assert len(form.buttons) == 2
assert form.buttons[0].title == 'Create Account'
assert form.buttons[1].title == 'Cancel'
def test_registration_getitem_found(dbsession): # noqa: F811
from ordr.models.account import Role, TokenSubject
from ordr.resources.account import (
RegistrationResource,
RegistrationTokenResource
)
request = DummyRequest(dbsession=dbsession)
user = get_example_user(Role.NEW)
token = user.issue_token(request, TokenSubject.REGISTRATION)
dbsession.add(user)
dbsession.flush()
parent = DummyResource(request=request)
resource = RegistrationResource('a name', parent)
result = resource[token.hash]
assert isinstance(result, RegistrationTokenResource)
assert result.__name__ == token.hash
assert result.__parent__ == resource
assert result.model == token
def test_registration_getitem_not_found(dbsession): # noqa: F811
from ordr.models.account import Role, TokenSubject
from ordr.resources.account import RegistrationResource
request = DummyRequest(dbsession=dbsession)
user = get_example_user(Role.NEW)
user.issue_token(request, TokenSubject.REGISTRATION)
dbsession.add(user)
dbsession.flush()
parent = DummyResource(request=request)
resource = RegistrationResource('a name', parent)
with pytest.raises(KeyError):
resource['unknown hash']
def test_registration_getitem_expired(dbsession): # noqa: F811
from ordr.models.account import Role, Token, TokenSubject
from ordr.resources.account import RegistrationResource
request = DummyRequest(dbsession=dbsession)
user = get_example_user(Role.NEW)
token = user.issue_token(request, TokenSubject.REGISTRATION)
token.expires = datetime.utcnow() - timedelta(weeks=1)
dbsession.add(user)
dbsession.flush()
parent = DummyResource(request=request)
resource = RegistrationResource('a name', parent)
with pytest.raises(KeyError):
resource[token.hash]
dbsession.flush()
assert dbsession.query(Token).count() == 0

44
tests/resources/base_child_resource.py

@ -7,23 +7,21 @@ from pyramid.testing import DummyRequest, DummyResource @@ -7,23 +7,21 @@ from pyramid.testing import DummyRequest, DummyResource
def test_base_child_init():
from ordr.resources.helpers import BaseChildResource
resource = BaseChildResource(
request='some request',
name='a name',
parent='the parent'
)
parent = DummyResource(request='some request')
resource = BaseChildResource(name='a name', parent=parent)
assert resource.__name__ == 'a name'
assert resource.__parent__ == 'the parent'
assert resource.__parent__ == parent
assert resource.request == 'some request'
def test_base_child_acl():
from ordr.resources.helpers import BaseChildResource
resource = BaseChildResource(
request='some request',
name='a name',
parent='the parent'
)
parent = DummyResource(request='some request')
resource = BaseChildResource(name='a name', parent=parent)
with pytest.raises(NotImplementedError):
resource.__acl__()
@ -32,10 +30,12 @@ def test_base_child_prepare_form(): @@ -32,10 +30,12 @@ def test_base_child_prepare_form():
from ordr.resources.helpers import BaseChildResource
from ordr.schemas.account import RegistrationSchema
import deform
parent = DummyResource()
request = DummyRequest()
resource = BaseChildResource(request, 'a name', parent)
parent = DummyResource(request=request)
resource = BaseChildResource('a name', parent)
form = resource._prepare_form(RegistrationSchema)
assert isinstance(form, deform.Form)
assert form.action == 'http://example.com//'
assert len(form.buttons) == 0
@ -44,10 +44,12 @@ def test_base_child_prepare_form(): @@ -44,10 +44,12 @@ def test_base_child_prepare_form():
def test_base_child_prepare_form_url():
from ordr.resources.helpers import BaseChildResource
from ordr.schemas.account import RegistrationSchema
parent = DummyResource()
request = DummyRequest()
resource = BaseChildResource(request, 'a name', parent)
parent = DummyResource(request=request)
resource = BaseChildResource('a name', parent)
form = resource._prepare_form(RegistrationSchema, action='/foo')
assert form.action == '/foo'
@ -55,11 +57,13 @@ def test_base_child_prepare_form_settings(): @@ -55,11 +57,13 @@ def test_base_child_prepare_form_settings():
from ordr.resources.helpers import BaseChildResource
from ordr.schemas.account import RegistrationSchema
import deform
parent = DummyResource()
request = DummyRequest()
resource = BaseChildResource(request, 'a name', parent)
parent = DummyResource(request=request)
resource = BaseChildResource('a name', parent)
settings = {'buttons': ('ok', 'cancel')}
form = resource._prepare_form(RegistrationSchema, **settings)
assert len(form.buttons) == 2
assert isinstance(form.buttons[0], deform.Button)
assert isinstance(form.buttons[1], deform.Button)
@ -68,15 +72,17 @@ def test_base_child_prepare_form_settings(): @@ -68,15 +72,17 @@ def test_base_child_prepare_form_settings():
def test_base_child_prepare_form_prefill():
from ordr.resources.helpers import BaseChildResource
from ordr.schemas.account import RegistrationSchema
parent = DummyResource()
request = DummyRequest()
resource = BaseChildResource(request, 'a name', parent)
parent = DummyResource(request=request)
resource = BaseChildResource('a name', parent)
prefill = {
'first_name': 'John',
'last_name': 'Doe',
'email': 'johndoe@example.com'
}
form = resource._prepare_form(RegistrationSchema, prefill=prefill)
assert form['first_name'].cstruct == 'John'
assert form['last_name'].cstruct == 'Doe'
assert form['email'].cstruct == 'johndoe@example.com'

2
tests/resources/root.py

@ -21,8 +21,10 @@ def test_root_acl(): @@ -21,8 +21,10 @@ def test_root_acl():
def test_root_getitem():
from ordr.resources import RootResource
from ordr.resources.account import RegistrationResource
root = RootResource(None)
child = root['register']
assert isinstance(child, RegistrationResource)
assert child.__name__ == 'register'
assert child.__parent__ == root

2
tests/schemas/__init__.py

@ -17,9 +17,9 @@ def test_csrf_schema_form_with_custom_url(): @@ -17,9 +17,9 @@ def test_csrf_schema_form_with_custom_url():
def test_csrf_schema_form_with_automatic_url():
''' test for creation with custom url '''
from ordr.schemas import CSRFSchema
root = DummyResource()
context = DummyResource('Crunchy', root)
request = DummyRequest(context=context, view_name='Frog')
form = CSRFSchema.as_form(request, buttons=['submit'])

16
tests/security.py

@ -7,6 +7,7 @@ from . import app_config, dbsession, get_example_user # noqa: F401 @@ -7,6 +7,7 @@ from . import app_config, dbsession, get_example_user # noqa: F401
def test_crypt_context_to_settings():
from ordr.security import crypt_context_settings_to_string
settings = {
'no_prefix': 'should not appear',
'prefix.something': 'left unchanged',
@ -20,30 +21,37 @@ def test_crypt_context_to_settings(): @@ -20,30 +21,37 @@ def test_crypt_context_to_settings():
'schemes = adjust,list',
'depreceated = do, not, adjust, this, list',
}
assert set(result.split('\n')) == expected_lines
def test_authentication_policy_authenticated_user_id_no_user():
from ordr.security import AuthenticationPolicy
ap = AuthenticationPolicy('')
request = DummyRequest(user=None)
assert ap.authenticated_userid(request) is None
def test_authentication_policy_authenticated_user_id_with_user():
from ordr.security import AuthenticationPolicy
from ordr.models import User
ap = AuthenticationPolicy('')
request = DummyRequest(user=User(id=123))
assert ap.authenticated_userid(request) == 123
def test_authentication_policy_effective_principals_no_user():
from ordr.security import AuthenticationPolicy
from pyramid.security import Everyone
request = DummyRequest(user=None)
ap = AuthenticationPolicy('')
result = ap.effective_principals(request)
assert result == [Everyone]
@ -51,6 +59,7 @@ def test_authentication_policy_effective_principals_with_user(): @@ -51,6 +59,7 @@ def test_authentication_policy_effective_principals_with_user():
from ordr.security import AuthenticationPolicy
from ordr.models import User, Role
from pyramid.security import Authenticated, Everyone
ap = AuthenticationPolicy('')
user = User(id=123, role=Role.PURCHASER)
request = DummyRequest(user=user)
@ -62,6 +71,7 @@ def test_authentication_policy_effective_principals_with_user(): @@ -62,6 +71,7 @@ def test_authentication_policy_effective_principals_with_user():
'role:purchaser',
'role:user'
]
assert result == expected
@ -75,14 +85,17 @@ def test_authentication_policy_effective_principals_with_user(): @@ -75,14 +85,17 @@ def test_authentication_policy_effective_principals_with_user():
def test_get_user_returns_user(dbsession, uauid, role_name):
from ordr.security import get_user
from ordr.models import Role
# this is a dirty hack, but DummyRequest does not accept setting an
# unauthenticated_userid
from pyramid.testing import DummyResource
request = DummyResource(unauthenticated_userid=uauid, dbsession=dbsession)
user_role = Role[role_name]
user = get_example_user(user_role)
dbsession.add(user)
dbsession.flush()
assert get_user(request) == user
@ -98,12 +111,15 @@ def test_get_user_returns_user(dbsession, uauid, role_name): @@ -98,12 +111,15 @@ def test_get_user_returns_user(dbsession, uauid, role_name):
def test_get_user_returns_none(dbsession, uauid, role_name):
from ordr.security import get_user
from ordr.models import Role
# this is a dirty hack, but DummyRequest does not accept setting an
# unauthenticated_userid
from pyramid.testing import DummyResource
request = DummyResource(unauthenticated_userid=uauid, dbsession=dbsession)
user_role = Role[role_name]
user = get_example_user(user_role)
dbsession.add(user)
dbsession.flush()
assert get_user(request) is None

2
tests/views/errors.py

@ -3,7 +3,9 @@ from pyramid.testing import DummyRequest @@ -3,7 +3,9 @@ from pyramid.testing import DummyRequest
def test_welcome():
from ordr.views.errors import notfound_view
request = DummyRequest()
result = notfound_view(None, request)
assert result == {}
assert request.response.status == '404 Not Found'

10
tests/views/pages.py

@ -14,8 +14,10 @@ from .. import app_config, dbsession, get_example_user # noqa: F401 @@ -14,8 +14,10 @@ from .. import app_config, dbsession, get_example_user # noqa: F401
)
def test_welcome(user, location):
from ordr.views.pages import welcome
request = DummyRequest(user=user)
result = welcome(None, request)
assert isinstance(result, HTTPFound)
assert result.location == f'http://example.com/{location}'
@ -37,11 +39,13 @@ def test_login(): @@ -37,11 +39,13 @@ def test_login():
)
def test_check_login_ok(dbsession, role):
from ordr.views.pages import check_login
user = get_example_user(role)
dbsession.add(user)
post_data = {'username': user.username, 'password': user.first_name}
request = DummyRequest(dbsession=dbsession, POST=post_data)
result = check_login(None, request)
assert isinstance(result, HTTPFound)
assert result.location == 'http://example.com//'
@ -51,11 +55,13 @@ def test_check_login_ok(dbsession, role): @@ -51,11 +55,13 @@ def test_check_login_ok(dbsession, role):
)
def test_check_login_not_activated(dbsession, role):
from ordr.views.pages import check_login
user = get_example_user(role)
dbsession.add(user)
post_data = {'username': user.username, 'password': user.first_name}
request = DummyRequest(dbsession=dbsession, POST=post_data)
result = check_login(None, request)
assert result == {'loginerror': True}
@ -70,17 +76,21 @@ def test_check_login_not_activated(dbsession, role): @@ -70,17 +76,21 @@ def test_check_login_not_activated(dbsession, role):
)
def test_check_login_invalid_credentials(dbsession, username, password):
from ordr.views.pages import check_login
user = get_example_user(Role.USER)
dbsession.add(user)
post_data = {'username': username, 'password': password}
request = DummyRequest(dbsession=dbsession, POST=post_data)
result = check_login(None, request)
assert result == {'loginerror': True}
def test_logout():
from ordr.views.pages import logout
request = DummyRequest()
result = logout(None, request)
assert isinstance(result, HTTPFound)
assert result.location == 'http://example.com//'

52
tests/views/registration.py

@ -2,9 +2,14 @@ import pytest @@ -2,9 +2,14 @@ import pytest
import deform
from pyramid.httpexceptions import HTTPFound
from pyramid.testing import DummyRequest
from pyramid.testing import DummyRequest, DummyResource
from .. import app_config, dbsession, get_post_request # noqa: F401
from .. import ( # noqa: F401
app_config,
dbsession,
get_example_user,
get_post_request
)
REGISTRATION_FORM_DATA = {
@ -26,7 +31,8 @@ def test_registration_form(): @@ -26,7 +31,8 @@ def test_registration_form():
from ordr.views.registration import registration_form
request = DummyRequest()
context = RegistrationResource(request=request, name=None, parent=None)
parent = DummyResource(request=request)
context = RegistrationResource(name=None, parent=parent)
result = registration_form(context, None)
form = result['form']
@ -41,12 +47,13 @@ def test_registration_form_valid(dbsession): # noqa: F811 @@ -41,12 +47,13 @@ def test_registration_form_valid(dbsession): # noqa: F811
data = REGISTRATION_FORM_DATA.copy()
request = get_post_request(dbsession, data)
context = RegistrationResource(request=request, name=None, parent=None)
parent = DummyResource(request=request)
context = RegistrationResource(name=None, parent=parent)
result = registration_form_processing(context, request)
# return value of function call
assert isinstance(result, HTTPFound)
assert result.location == 'http://example.com/verify'
assert result.location == 'http://example.com//verify'
# user should be added to database
user = dbsession.query(User).first()
@ -69,20 +76,51 @@ def test_registration_form_valid(dbsession): # noqa: F811 @@ -69,20 +76,51 @@ def test_registration_form_valid(dbsession): # noqa: F811
def test_registration_form_invalid(dbsession): # noqa: F811
from ordr.views.registration import registration_form_processing
from ordr.resources.account import RegistrationResource
data = REGISTRATION_FORM_DATA.copy()
data['email'] = 'not an email address'
request = get_post_request(dbsession, data)
context = RegistrationResource(request=request, name=None, parent=None)
parent = DummyResource(request=request)
context = RegistrationResource(name=None, parent=parent)
result = registration_form_processing(context, request)
assert result['form'].error is not None
def test_registration_form_no_create_button(dbsession): # noqa: F811
from ordr.views.registration import registration_form_processing
from ordr.resources.account import RegistrationResource
data = REGISTRATION_FORM_DATA.copy()
data.pop('create')
request = get_post_request(dbsession, data)
context = RegistrationResource(request=request, name=None, parent=None)
parent = DummyResource(request=request)
context = RegistrationResource(name=None, parent=parent)
result = registration_form_processing(context, request)
assert result.location == 'http://example.com//'
def test_registration_verify():
from ordr.views.registration import verify
result = verify(None, None)
assert result == {}
def test_registration_completed(dbsession): # noqa: F811
from ordr.models.account import User, Role, Token, TokenSubject
from ordr.views.registration import completed
request = DummyRequest(dbsession=dbsession)
user = get_example_user(Role.UNVALIDATED)
user.issue_token(request, TokenSubject.REGISTRATION)
dbsession.add(user)
dbsession.flush()
token = user.tokens[0]
context = DummyResource(model=token)
result = completed(context, request)
assert result == {}
assert user.role == Role.NEW
assert dbsession.query(Token).count() == 0
assert dbsession.query(User).count() == 1