From 1c96f9b97033b8a9f795801980aa911fd5397845 Mon Sep 17 00:00:00 2001 From: Holger Frey Date: Thu, 19 Apr 2018 18:05:02 +0200 Subject: [PATCH] finished registration workflow --- ordr/events.py | 4 + ordr/models/account.py | 29 ++++++ ordr/resources/__init__.py | 2 +- ordr/resources/account.py | 22 +++++ ordr/resources/helpers.py | 7 +- ordr/schemas/account.py | 6 +- ordr/security.py | 2 + .../account/registration_completed.jinja2 | 36 ++++++++ .../account/registration_form.jinja2 | 4 +- .../account/registration_verify.jinja2 | 35 +++++++ ordr/templates/layout.jinja2 | 4 +- ordr/views/pages.py | 2 + ordr/views/registration.py | 28 ++++++ tests/__init__.py | 5 + tests/_functional/__init__.py | 27 +++++- tests/_functional/errors.py | 4 +- tests/_functional/layout.py | 20 ++-- tests/_functional/login_logout.py | 34 ++++--- tests/_functional/pages.py | 14 +-- tests/_functional/registration.py | 44 ++++++++- tests/models/account.py | 14 +++ tests/resources/account.py | 91 ++++++++++++++++++- tests/resources/base_child_resource.py | 44 +++++---- tests/resources/root.py | 2 + tests/schemas/__init__.py | 2 +- tests/security.py | 16 ++++ tests/views/errors.py | 2 + tests/views/pages.py | 10 ++ tests/views/registration.py | 52 +++++++++-- 29 files changed, 485 insertions(+), 77 deletions(-) create mode 100644 ordr/templates/account/registration_completed.jinja2 create mode 100644 ordr/templates/account/registration_verify.jinja2 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 %} +
+
+

Registration

+
+
+
+
+

+ Step 1: Registration +

+
+
+

+ Step 2: Validate Email +

+
+
+

+ Step 3: Finished +

+
+
+
+
+

Registration Completed

+

Thank you for verifying your email address.

+

Before you can start ordering, an administrator must activate your account

+

You'll receive an email when your account is activated

+
+
+{% endblock content %} diff --git a/ordr/templates/account/registration_form.jinja2 b/ordr/templates/account/registration_form.jinja2 index b8809e1..dc570b0 100644 --- a/ordr/templates/account/registration_form.jinja2 +++ b/ordr/templates/account/registration_form.jinja2 @@ -1,9 +1,11 @@ {% extends "ordr:templates/layout.jinja2" %} +{% block title %} Ordr | Registration {% endblock title %} + {% block content %}
-

Registration

+

Registration

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 %} +
+
+

Registration

+
+
+
+
+

+ Step 1: Registration +

+
+
+

+ Step 2: Validate Email +

+
+
+

+ Step 3: Finished +

+
+
+
+
+

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