diff --git a/ordr/events.py b/ordr/events.py
index 3bbfb60..bb4b9fb 100644
--- a/ordr/events.py
+++ b/ordr/events.py
@@ -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'
diff --git a/ordr/models/account.py b/ordr/models/account.py
index 6fd0b55..87da1eb 100644
--- a/ordr/models/account.py
+++ b/ordr/models/account.py
@@ -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
diff --git a/ordr/resources/__init__.py b/ordr/resources/__init__.py
index 5f13291..09121e9 100644
--- a/ordr/resources/__init__.py
+++ b/ordr/resources/__init__.py
@@ -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):
diff --git a/ordr/resources/account.py b/ordr/resources/account.py
index 34408d6..3173ee8 100644
--- a/ordr/resources/account.py
+++ b/ordr/resources/account.py
@@ -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):
''' 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 = {
diff --git a/ordr/resources/helpers.py b/ordr/resources/helpers.py
index 15c84af..971ae95 100644
--- a/ordr/resources/helpers.py
+++ b/ordr/resources/helpers.py
@@ -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 '''
diff --git a/ordr/schemas/account.py b/ordr/schemas/account.py
index 1e6f838..56f2730 100644
--- a/ordr/schemas/account.py
+++ b/ordr/schemas/account.py
@@ -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):
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(),
diff --git a/ordr/security.py b/ordr/security.py
index 99d08ba..e4bf41c 100644
--- a/ordr/security.py
+++ b/ordr/security.py
@@ -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'])
diff --git a/ordr/templates/account/registration_completed.jinja2 b/ordr/templates/account/registration_completed.jinja2
new file mode 100644
index 0000000..57c5b63
--- /dev/null
+++ b/ordr/templates/account/registration_completed.jinja2
@@ -0,0 +1,36 @@
+{% extends "ordr:templates/layout.jinja2" %}
+
+{% block title %} Ordr | Registration {% endblock title %}
+
+{% block content %}
+
diff --git a/ordr/templates/account/registration_verify.jinja2 b/ordr/templates/account/registration_verify.jinja2
new file mode 100644
index 0000000..72c77a4
--- /dev/null
+++ b/ordr/templates/account/registration_verify.jinja2
@@ -0,0 +1,35 @@
+{% extends "ordr:templates/layout.jinja2" %}
+
+{% block title %} Ordr | Registration {% endblock title %}
+
+{% block content %}
+
+
+
+
+ Step 1: Registration
+
+
+
+
+ Step 2: Validate Email
+
+
+
+
+
+
+
Verify Your Email Address
+
To complete the registration process an email has been sent to you.
+
Please follow the link in the email to verify your address and complete the registration process.
+
+
+{% endblock content %}
diff --git a/ordr/templates/layout.jinja2 b/ordr/templates/layout.jinja2
index e2c5ce4..391a260 100644
--- a/ordr/templates/layout.jinja2
+++ b/ordr/templates/layout.jinja2
@@ -8,9 +8,7 @@
- {% block title %}
-
Ordr
- {% endblock title %}
+
{% block title %} Ordr {% endblock title %}
diff --git a/ordr/views/pages.py b/ordr/views/pages.py
index 1b4f5b8..b235f21 100644
--- a/ordr/views/pages.py
+++ b/ordr/views/pages.py
@@ -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}
diff --git a/ordr/views/registration.py b/ordr/views/registration.py
index 30252cf..a1af415 100644
--- a/ordr/views/registration.py
+++ b/ordr/views/registration.py
@@ -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):
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 {}
diff --git a/tests/__init__.py b/tests/__init__.py
index 03db5b5..c12ecba 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -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):
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)
diff --git a/tests/_functional/__init__.py b/tests/_functional/__init__.py
index e66230f..41d341c 100644
--- a/tests/_functional/__init__.py
+++ b/tests/_functional/__init__.py
@@ -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):
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):
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):
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():
@pytest.fixture(scope='function')
def testapp(testappsetup):
+ ''' fixture using webtests, resets the logged every time '''
testappsetup.reset()
yield testappsetup
diff --git a/tests/_functional/errors.py b/tests/_functional/errors.py
index 2e61036..fd4ee3f 100644
--- a/tests/_functional/errors.py
+++ b/tests/_functional/errors.py
@@ -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
diff --git a/tests/_functional/layout.py b/tests/_functional/layout.py
index c16fc41..77bef58 100644
--- a/tests/_functional/layout.py
+++ b/tests/_functional/layout.py
@@ -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
)
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
diff --git a/tests/_functional/login_logout.py b/tests/_functional/login_logout.py
index 7c6d02b..c3415d9 100644
--- a/tests/_functional/login_logout.py
+++ b/tests/_functional/login_logout.py
@@ -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
[('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
diff --git a/tests/_functional/pages.py b/tests/_functional/pages.py
index fd154ad..117b774 100644
--- a/tests/_functional/pages.py
+++ b/tests/_functional/pages.py
@@ -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'
diff --git a/tests/_functional/registration.py b/tests/_functional/registration.py
index 44b5c07..49ab271 100644
--- a/tests/_functional/registration.py
+++ b/tests/_functional/registration.py
@@ -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
diff --git a/tests/models/account.py b/tests/models/account.py
index a57327c..b079208 100644
--- a/tests/models/account.py
+++ b/tests/models/account.py
@@ -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):
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():
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():
)
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__():
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
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
)
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
diff --git a/tests/resources/account.py b/tests/resources/account.py
index ca5c721..85187d7 100644
--- a/tests/resources/account.py
+++ b/tests/resources/account.py
@@ -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():
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
diff --git a/tests/resources/base_child_resource.py b/tests/resources/base_child_resource.py
index 40c98b0..86ce48e 100644
--- a/tests/resources/base_child_resource.py
+++ b/tests/resources/base_child_resource.py
@@ -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():
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():
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():
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():
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'
diff --git a/tests/resources/root.py b/tests/resources/root.py
index 8c6ff63..769d948 100644
--- a/tests/resources/root.py
+++ b/tests/resources/root.py
@@ -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
diff --git a/tests/schemas/__init__.py b/tests/schemas/__init__.py
index 59d25aa..4dddca8 100644
--- a/tests/schemas/__init__.py
+++ b/tests/schemas/__init__.py
@@ -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'])
diff --git a/tests/security.py b/tests/security.py
index 9c027ae..9f3a200 100644
--- a/tests/security.py
+++ b/tests/security.py
@@ -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():
'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():
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():
'role:purchaser',
'role:user'
]
+
assert result == expected
@@ -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):
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
diff --git a/tests/views/errors.py b/tests/views/errors.py
index 1ea30e4..be1c43d 100644
--- a/tests/views/errors.py
+++ b/tests/views/errors.py
@@ -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'
diff --git a/tests/views/pages.py b/tests/views/pages.py
index d9980f4..60c370a 100644
--- a/tests/views/pages.py
+++ b/tests/views/pages.py
@@ -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():
)
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):
)
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):
)
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//'
diff --git a/tests/views/registration.py b/tests/views/registration.py
index 50cff93..0930f70 100644
--- a/tests/views/registration.py
+++ b/tests/views/registration.py
@@ -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():
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
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
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