From e5111dbb0c70f42787d1ef23b0d6b9fce4720af1 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Fri, 14 Mar 2014 15:26:20 -0400 Subject: [PATCH] Add moar tests! --- tests/conftest.py | 64 +++-------- .../custom_security/change_password.html | 2 + .../custom_security/forgot_password.html | 2 + .../templates/custom_security/login_user.html | 2 + .../custom_security/register_user.html | 2 + .../custom_security/reset_password.html | 2 + .../custom_security/send_confirmation.html | 2 + .../templates/custom_security/send_login.html | 2 + .../security/email/reset_instructions.html | 3 + tests/test_changeable.py | 18 ++- tests/test_common.py | 38 +++++-- tests/test_context_processors.py | 106 ++++++++++++++++++ tests/test_datastore.py | 80 ++++++++++++- tests/test_misc.py | 34 ++++++ tests/test_passwordless.py | 2 +- tests/test_registerable.py | 11 +- tests/utils.py | 10 +- 17 files changed, 316 insertions(+), 64 deletions(-) create mode 100644 tests/templates/security/email/reset_instructions.html create mode 100644 tests/test_context_processors.py diff --git a/tests/conftest.py b/tests/conftest.py index 9fb514a..62e5f25 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ import time import pytest -from flask import Flask, render_template, current_app +from flask import Flask, render_template from flask_mail import Mail from flask_security import Security, MongoEngineUserDatastore, SQLAlchemyUserDatastore, \ @@ -59,7 +59,7 @@ def app(): def http_custom_realm(): return render_template('index.html', content='HTTP Authentication') - @app.route('/token') + @app.route('/token', methods=['GET', 'POST']) @auth_token_required def token(): return render_template('index.html', content='Token Authentication') @@ -96,40 +96,6 @@ def app(): def unauthorized(): return render_template('unauthorized.html') - @app.route('/coverage/add_role_to_user') - def add_role_to_user(): - ds = current_app.security.datastore - u = ds.find_user(email='joe@lp.com') - r = ds.find_role('admin') - ds.add_role_to_user(u, r) - return 'success' - - @app.route('/coverage/remove_role_from_user') - def remove_role_from_user(): - ds = current_app.security.datastore - u = ds.find_user(email='matt@lp.com') - ds.remove_role_from_user(u, 'admin') - return 'success' - - @app.route('/coverage/deactivate_user') - def deactivate_user(): - ds = current_app.security.datastore - u = ds.find_user(email='matt@lp.com') - ds.deactivate_user(u) - return 'success' - - @app.route('/coverage/activate_user') - def activate_user(): - ds = current_app.security.datastore - u = ds.find_user(email='tiya@lp.com') - ds.activate_user(u) - return 'success' - - @app.route('/coverage/invalid_role') - def invalid_role(): - ds = current_app.security.datastore - return 'success' if ds.find_role('bogus') is None else 'failure' - @app.route('/page1') def page_1(): return 'Page 1' @@ -158,7 +124,7 @@ def mongoengine_datastore(request, app): class User(db.Document, UserMixin): email = db.StringField(unique=True, max_length=255) username = db.StringField(max_length=255) - password = db.StringField(required=True, max_length=255) + password = db.StringField(required=False, max_length=255) last_login_at = db.DateTimeField() current_login_at = db.DateTimeField() last_login_ip = db.StringField(max_length=100) @@ -237,7 +203,7 @@ def peewee_datastore(request, app, tmpdir): class User(db.Model, UserMixin): email = TextField() username = TextField() - password = TextField() + password = TextField(null=True) last_login_at = DateTimeField(null=True) current_login_at = DateTimeField(null=True) last_login_ip = TextField(null=True) @@ -287,14 +253,9 @@ def mongoengine_app(app, mongoengine_datastore): return create -@pytest.fixture(params=['sqlalchemy', 'mongoengine', 'peewee']) -def client(request, sqlalchemy_app, mongoengine_app, peewee_app): - if request.param == 'sqlalchemy': - app = sqlalchemy_app() - elif request.param == 'mongoengine': - app = mongoengine_app() - elif request.param == 'peewee': - app = peewee_app() +@pytest.fixture() +def client(request, sqlalchemy_app): + app = sqlalchemy_app() populate_data(app) return app.test_client() @@ -305,3 +266,14 @@ def get_message(app): rv = app.config['SECURITY_MSG_' + key][0] % kwargs return rv.encode('utf-8') return fn + + +@pytest.fixture(params=['sqlalchemy', 'mongoengine', 'peewee']) +def datastore(request, sqlalchemy_datastore, mongoengine_datastore, peewee_datastore): + if request.param == 'sqlalchemy': + rv = sqlalchemy_datastore + elif request.param == 'mongoengine': + rv = mongoengine_datastore + elif request.param == 'peewee': + rv = peewee_datastore + return rv diff --git a/tests/templates/custom_security/change_password.html b/tests/templates/custom_security/change_password.html index f9f7d23..75ab2c0 100644 --- a/tests/templates/custom_security/change_password.html +++ b/tests/templates/custom_security/change_password.html @@ -1 +1,3 @@ CUSTOM CHANGE PASSWORD + +{{ foo }} diff --git a/tests/templates/custom_security/forgot_password.html b/tests/templates/custom_security/forgot_password.html index e268b44..5cdf923 100644 --- a/tests/templates/custom_security/forgot_password.html +++ b/tests/templates/custom_security/forgot_password.html @@ -1 +1,3 @@ CUSTOM FORGOT PASSWORD + +{{ foo }} diff --git a/tests/templates/custom_security/login_user.html b/tests/templates/custom_security/login_user.html index abbe2c1..504db8a 100644 --- a/tests/templates/custom_security/login_user.html +++ b/tests/templates/custom_security/login_user.html @@ -1 +1,3 @@ CUSTOM LOGIN USER + +{{ foo }} diff --git a/tests/templates/custom_security/register_user.html b/tests/templates/custom_security/register_user.html index 927c1f0..2dbca0c 100644 --- a/tests/templates/custom_security/register_user.html +++ b/tests/templates/custom_security/register_user.html @@ -1 +1,3 @@ CUSTOM REGISTER USER + +{{ foo }} diff --git a/tests/templates/custom_security/reset_password.html b/tests/templates/custom_security/reset_password.html index dc8e874..d4c21fe 100644 --- a/tests/templates/custom_security/reset_password.html +++ b/tests/templates/custom_security/reset_password.html @@ -1 +1,3 @@ CUSTOM RESET PASSWORD + +{{ foo }} diff --git a/tests/templates/custom_security/send_confirmation.html b/tests/templates/custom_security/send_confirmation.html index a01892d..d4df61d 100644 --- a/tests/templates/custom_security/send_confirmation.html +++ b/tests/templates/custom_security/send_confirmation.html @@ -1 +1,3 @@ CUSTOM SEND CONFIRMATION + +{{ foo }} diff --git a/tests/templates/custom_security/send_login.html b/tests/templates/custom_security/send_login.html index 7ef81de..3685913 100644 --- a/tests/templates/custom_security/send_login.html +++ b/tests/templates/custom_security/send_login.html @@ -1 +1,3 @@ CUSTOM SEND LOGIN + +{{ foo }} diff --git a/tests/templates/security/email/reset_instructions.html b/tests/templates/security/email/reset_instructions.html new file mode 100644 index 0000000..aa0937e --- /dev/null +++ b/tests/templates/security/email/reset_instructions.html @@ -0,0 +1,3 @@ +CUSTOM RESET INSTRUCTIONS + +{{ foo }} diff --git a/tests/test_changeable.py b/tests/test_changeable.py index fe4734e..66e9cba 100644 --- a/tests/test_changeable.py +++ b/tests/test_changeable.py @@ -49,16 +49,22 @@ def test_recoverable_flag(app, sqlalchemy_datastore, get_message): 'new_password': 'newpassword', 'new_password_confirm': 'notnewpassword' }, follow_redirects=True) - assert get_message('PASSWORD_CHANGE') not in response.data assert get_message('RETYPE_PASSWORD_MISMATCH') in response.data + # Test missing password + response = client.post('/change', data={ + 'password': ' ', + 'new_password': '', + 'new_password_confirm': '' + }, follow_redirects=True) + assert get_message('PASSWORD_NOT_PROVIDED') in response.data + # Test bad password response = client.post('/change', data={ 'password': 'password', 'new_password': 'a', 'new_password_confirm': 'a' }, follow_redirects=True) - assert get_message('PASSWORD_CHANGE') not in response.data assert get_message('PASSWORD_INVALID_LENGTH') in response.data # Test same as previous @@ -67,7 +73,6 @@ def test_recoverable_flag(app, sqlalchemy_datastore, get_message): 'new_password': 'password', 'new_password_confirm': 'password' }, follow_redirects=True) - assert get_message('PASSWORD_CHANGE') not in response.data assert get_message('PASSWORD_IS_THE_SAME') in response.data # Test successful submit sends email notification @@ -84,6 +89,13 @@ def test_recoverable_flag(app, sqlalchemy_datastore, get_message): assert len(outbox) == 1 assert "Your password has been changed" in outbox[0].html + # Test JSON + data = ('{"password": "newpassword", "new_password": "newpassword2", ' + '"new_password_confirm": "newpassword2"}') + response = client.post('/change', data=data, headers={'Content-Type': 'application/json'}) + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + def test_custom_change_url(app, sqlalchemy_datastore, get_message): client = _get_client(app, sqlalchemy_datastore, **{ diff --git a/tests/test_common.py b/tests/test_common.py index 856a577..ffee454 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -28,6 +28,18 @@ def test_authenticate(client): assert b'Hello matt@lp.com' in response.data +def test_authenticate_with_next(client): + data = dict(email='matt@lp.com', password='password') + response = client.post('/login?next=/page1', data=data, follow_redirects=True) + assert b'Page 1' in response.data + + +def test_authenticate_with_invalid_next(client, get_message): + data = dict(email='matt@lp.com', password='password') + response = client.post('/login?next=http://google.com', data=data) + assert get_message('INVALID_REDIRECT') in response.data + + def test_authenticate_case_insensitive_email(client): response = authenticate(client, email='MATT@lp.com', follow_redirects=True) assert b'Hello matt@lp.com' in response.data @@ -58,6 +70,11 @@ def test_inactive_user(client, get_message): assert get_message('DISABLED_ACCOUNT') in response.data +def test_unset_password(client, get_message): + response = authenticate(client, "jess@lp.com", "password") + assert get_message('PASSWORD_NOT_SET') in response.data + + def test_logout(client): authenticate(client) response = logout(client, follow_redirects=True) @@ -199,6 +216,9 @@ def test_multi_auth_basic(client): }) assert b'Basic' in response.data + response = client.get('/multi_auth') + assert response.status_code == 401 + def test_multi_auth_token(client): response = json_authenticate(client) @@ -245,12 +265,12 @@ def test_token_loader_does_not_fail_with_invalid_token(client): assert b'BadSignature' not in response.data -def test_coverage_endpoints(client): - for endpoint in [ - '/coverage/add_role_to_user', - '/coverage/remove_role_from_user', - '/coverage/activate_user', - '/coverage/deactivate_user' - ]: - response = client.get(endpoint) - assert b'success' in response.data +def test_sending_auth_token_with_json(client): + response = json_authenticate(client) + token = response.jdata['response']['user']['authentication_token'] + data = '{"auth_token": "%s"}' % token + response = client.post('/token', data=data, headers={'Content-Type': 'application/json'}) + assert b'Token Authentication' in response.data + assert b'Token Authentication' in response.data + + diff --git a/tests/test_context_processors.py b/tests/test_context_processors.py new file mode 100644 index 0000000..34cdfdf --- /dev/null +++ b/tests/test_context_processors.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +""" + test_context_processors + ~~~~~~~~~~~~~~~~~~~~~~~ + + Context processor tests +""" + +from utils import authenticate, init_app_with_options + + +def test_context_processors(app, sqlalchemy_datastore): + init_app_with_options(app, sqlalchemy_datastore, **{ + 'SECURITY_RECOVERABLE': True, + # 'SECURITY_PASSWORDLESS': True, + 'SECURITY_REGISTERABLE': True, + 'SECURITY_CONFIRMABLE': True, + 'SECURITY_CHANGEABLE': True, + 'SECURITY_LOGIN_WITHOUT_CONFIRMATION': True, + 'SECURITY_CHANGE_PASSWORD_TEMPLATE': 'custom_security/change_password.html', + 'SECURITY_LOGIN_USER_TEMPLATE': 'custom_security/login_user.html', + # 'SECURITY_SEND_LOGIN_TEMPLATE': 'custom_security/send_login.html', + 'SECURITY_RESET_PASSWORD_TEMPLATE': 'custom_security/reset_password.html', + 'SECURITY_FORGOT_PASSWORD_TEMPLATE': 'custom_security/forgot_password.html', + 'SECURITY_SEND_CONFIRMATION_TEMPLATE': 'custom_security/send_confirmation.html', + 'SECURITY_REGISTER_USER_TEMPLATE': 'custom_security/register_user.html' + }) + + client = app.test_client() + + @app.security.forgot_password_context_processor + def forgot_password(): + return {'foo': 'bar'} + + response = client.get('/reset') + assert b'bar' in response.data + + @app.security.login_context_processor + def login(): + return {'foo': 'bar'} + + response = client.get('/login') + assert b'bar' in response.data + + @app.security.register_context_processor + def register(): + return {'foo': 'bar'} + + response = client.get('/register') + assert b'bar' in response.data + + @app.security.reset_password_context_processor + def reset_password(): + return {'foo': 'bar'} + + response = client.get('/reset') + assert b'bar' in response.data + + @app.security.change_password_context_processor + def change_password(): + return {'foo': 'bar'} + + authenticate(client) + response = client.get('/change') + assert b'bar' in response.data + + @app.security.send_confirmation_context_processor + def send_confirmation(): + return {'foo': 'bar'} + + response = client.get('/confirm') + assert b'bar' in response.data + + @app.security.mail_context_processor + def mail(): + return {'foo': 'bar'} + + with app.mail.record_messages() as outbox: + client.post('/reset', data=dict(email='matt@lp.com')) + + email = outbox[0] + assert b'bar' in email.html + + +def test_passwordless_login_context_processor(app, sqlalchemy_datastore): + init_app_with_options(app, sqlalchemy_datastore, **{ + 'SECURITY_PASSWORDLESS': True, + 'SECURITY_SEND_LOGIN_TEMPLATE': 'custom_security/send_login.html', + }) + + client = app.test_client() + + @app.security.send_login_context_processor + def send_login(): + return {'foo': 'bar'} + + response = client.get('/login') + assert b'bar' in response.data + + # @app.security.mail_context_processor + # def mail(): + # return {'foo': 'bar'} + + + + diff --git a/tests/test_datastore.py b/tests/test_datastore.py index 028e230..d75ab49 100644 --- a/tests/test_datastore.py +++ b/tests/test_datastore.py @@ -8,20 +8,28 @@ from pytest import raises -from flask_security import UserMixin +from flask_security import UserMixin, RoleMixin from flask_security.datastore import Datastore, UserDatastore +from utils import init_app_with_options + class User(UserMixin): pass +class Role(RoleMixin): + pass + + def test_unimplemented_datastore_methods(): datastore = Datastore(None) + assert datastore.db is None with raises(NotImplementedError): datastore.put(None) with raises(NotImplementedError): datastore.delete(None) + assert not datastore.commit() def test_unimplemented_user_datastore_methods(): @@ -30,6 +38,8 @@ def test_unimplemented_user_datastore_methods(): datastore.find_user(None) with raises(NotImplementedError): datastore.find_role(None) + with raises(NotImplementedError): + datastore.get_user(None) def test_toggle_active(): @@ -70,3 +80,71 @@ def test_activate_returns_false_if_already_true(): user = User() user.active = True assert not datastore.activate_user(user) + + +def test_get_user(app, datastore): + init_app_with_options(app, datastore, **{ + 'SECURITY_USER_IDENTITY_ATTRIBUTES': ('email', 'username') + }) + + with app.app_context(): + user_id = datastore.find_user(email='matt@lp.com').id + + user = datastore.get_user(user_id) + assert user is not None + + user = datastore.get_user('matt@lp.com') + assert user is not None + + user = datastore.get_user('matt') + assert user is not None + + +def test_find_role(app, datastore): + init_app_with_options(app, datastore) + + role = datastore.find_role('admin') + assert role is not None + + role = datastore.find_role('bogus') + assert role is None + + +def test_add_role_to_user(app, datastore): + init_app_with_options(app, datastore) + + # Test with user object + user = datastore.find_user(email='matt@lp.com') + assert user.has_role('editor') is False + assert datastore.add_role_to_user(user, 'editor') is True + assert datastore.add_role_to_user(user, 'editor') is False + assert user.has_role('editor') is True + + # Test with email + assert datastore.add_role_to_user('jill@lp.com', 'editor') is True + user = datastore.find_user(email='jill@lp.com') + assert user.has_role('editor') is True + + # Test remove role + assert datastore.remove_role_from_user(user, 'editor') is True + assert datastore.remove_role_from_user(user, 'editor') is False + + +def test_create_user_with_roles(app, datastore): + init_app_with_options(app, datastore) + + user = datastore.create_user(email='dude@lp.com', username='dude', + password='password', roles=['admin']) + datastore.commit() + user = datastore.find_user(email='dude@lp.com') + assert user.has_role('admin') is True + + +def test_delete_user(app, datastore): + init_app_with_options(app, datastore) + + user = datastore.find_user(email='matt@lp.com') + datastore.delete_user(user) + datastore.commit() + user = datastore.find_user(email='matt@lp.com') + assert user is None diff --git a/tests/test_misc.py b/tests/test_misc.py index 83ff3e7..8649c36 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -6,6 +6,8 @@ Email functionality tests """ +from pytest import raises + from flask_security import Security from flask_security.forms import LoginForm, RegisterForm, ConfirmRegisterForm, \ SendConfirmationForm, PasswordlessLoginForm, ForgotPasswordForm, ResetPasswordForm, \ @@ -140,3 +142,35 @@ def test_addition_identity_attributes(app, sqlalchemy_datastore): client = app.test_client() response = authenticate(client, email='matt', follow_redirects=True) assert b'Hello matt@lp.com' in response.data + + +def test_flash_messages_off(app, sqlalchemy_datastore, get_message): + init_app_with_options(app, sqlalchemy_datastore, **{ + 'SECURITY_FLASH_MESSAGES': False + }) + client = app.test_client() + response = client.get('/profile') + assert get_message('LOGIN') not in response.data + + +def test_invalid_hash_scheme(app, sqlalchemy_datastore, get_message): + with raises(ValueError): + init_app_with_options(app, sqlalchemy_datastore, **{ + 'SECURITY_PASSWORD_HASH': 'bogus' + }) + + +def test_change_hash_type(app, sqlalchemy_datastore): + init_app_with_options(app, sqlalchemy_datastore, **{ + 'SECURITY_PASSWORD_SCHEMES': ['bcrypt', 'plaintext'] + }) + + app.config['SECURITY_PASSWORD_HASH'] = 'bcrypt' + app.config['SECURITY_PASSWORD_SALT'] = 'salty' + + app.security = Security(app, datastore=sqlalchemy_datastore, register_blueprint=False) + + client = app.test_client() + + response = client.post('/login', data=dict(email='matt@lp.com', password='password')) + assert response.status_code == 302 diff --git a/tests/test_passwordless.py b/tests/test_passwordless.py index 7734072..7cf964a 100644 --- a/tests/test_passwordless.py +++ b/tests/test_passwordless.py @@ -40,7 +40,7 @@ def test_trackable_flag(app, sqlalchemy_datastore, get_message): # Test login with json and invalid email data = '{"email": "nobody@lp.com", "password": "password"}' response = client.post('/login', data=data, headers={'Content-Type': 'application/json'}) - assert 'errors' in response.data + assert b'errors' in response.data # Test sends email and shows appropriate response with capture_passwordless_login_requests() as requests: diff --git a/tests/test_registerable.py b/tests/test_registerable.py index a45ba96..09acaf4 100644 --- a/tests/test_registerable.py +++ b/tests/test_registerable.py @@ -57,12 +57,21 @@ def test_registerable_flag(app, sqlalchemy_datastore, get_message): # Test registering with JSON data = '{ "email": "dude2@lp.com", "password": "password"}' - response = client.post('/register', data=data, content_type='application/json') + response = client.post('/register', data=data, headers={'Content-Type': 'application/json'}) assert response.headers['content-type'] == 'application/json' assert response.jdata['meta']['code'] == 200 logout(client) + # Test registering with invalid JSON + data = '{ "email": "bogus", "password": "password"}' + response = client.post('/register', data=data, headers={'Content-Type': 'application/json'}) + print response.data + assert response.headers['content-type'] == 'application/json' + assert response.jdata['meta']['code'] == 400 + + logout(client) + # Test ?next param data = dict(email='dude3@lp.com', password='password', diff --git a/tests/utils.py b/tests/utils.py index 17de43f..b05ddf7 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -39,11 +39,14 @@ def create_users(ds, count=None): ('joe@lp.com', 'joe', 'password', ['editor'], True), ('dave@lp.com', 'dave', 'password', ['admin', 'editor'], True), ('jill@lp.com', 'jill', 'password', ['author'], True), - ('tiya@lp.com', 'tiya', 'password', [], False)] + ('tiya@lp.com', 'tiya', 'password', [], False), + ('jess@lp.com', 'jess', None, [], True)] count = count or len(users) for u in users[:count]: - pw = encrypt_password(u[2]) + pw = u[2] + if pw is not None: + pw = encrypt_password(pw) roles = [ds.find_or_create_role(rn) for rn in u[3]] ds.commit() user = ds.create_user(email=u[0], username=u[1], password=pw, active=u[4]) @@ -75,6 +78,7 @@ class Response(BaseResponse): # pragma: no cover def init_app_with_options(app, datastore, **options): + security_args = options.pop('security_args', {}) app.config.update(**options) - app.security = Security(app, datastore=datastore) + app.security = Security(app, datastore=datastore, **security_args) populate_data(app)