diff options
-rw-r--r-- | docs/hackinghowto.rst | 2 | ||||
-rw-r--r-- | mediagoblin/app.py | 13 | ||||
-rw-r--r-- | mediagoblin/auth/forms.py | 4 | ||||
-rw-r--r-- | mediagoblin/auth/lib.py | 25 | ||||
-rw-r--r-- | mediagoblin/auth/routing.py | 9 | ||||
-rw-r--r-- | mediagoblin/auth/views.py | 85 | ||||
-rw-r--r-- | mediagoblin/celery_setup/from_tests.py | 43 | ||||
-rw-r--r-- | mediagoblin/decorators.py | 6 | ||||
-rw-r--r-- | mediagoblin/edit/routing.py | 4 | ||||
-rw-r--r-- | mediagoblin/edit/views.py | 23 | ||||
-rw-r--r-- | mediagoblin/submit/routing.py | 4 | ||||
-rw-r--r-- | mediagoblin/submit/views.py | 25 | ||||
-rw-r--r-- | mediagoblin/templates/mediagoblin/auth/login.html | 3 | ||||
-rw-r--r-- | mediagoblin/tests/mgoblin_test_app.ini | 46 | ||||
-rw-r--r-- | mediagoblin/tests/test_auth.py | 172 | ||||
-rw-r--r-- | mediagoblin/tests/test_tests.py | 38 | ||||
-rw-r--r-- | mediagoblin/tests/tools.py | 109 | ||||
-rw-r--r-- | mediagoblin/user_pages/routing.py | 2 | ||||
-rw-r--r-- | mediagoblin/user_pages/views.py | 30 | ||||
-rw-r--r-- | mediagoblin/util.py | 114 | ||||
-rw-r--r-- | mediagoblin/views.py | 21 | ||||
-rw-r--r-- | setup.py | 1 |
22 files changed, 625 insertions, 154 deletions
diff --git a/docs/hackinghowto.rst b/docs/hackinghowto.rst index a56498bb..a9aadb62 100644 --- a/docs/hackinghowto.rst +++ b/docs/hackinghowto.rst @@ -152,7 +152,7 @@ Running the test suite Run:: - ./bin/nosetests + CELERY_CONFIG_MODULE=mediagoblin.celery_setup.from_tests ./bin/nosetests Running a shell diff --git a/mediagoblin/app.py b/mediagoblin/app.py index 714404de..e5949531 100644 --- a/mediagoblin/app.py +++ b/mediagoblin/app.py @@ -139,12 +139,13 @@ def paste_app_factory(global_config, **app_config): raise ImproperlyConfigured( "One of direct_remote_path or direct_remote_paths must be provided") - if asbool(os.environ.get('CELERY_ALWAYS_EAGER')): - setup_celery_from_config( - app_config, global_config, - force_celery_always_eager=True) - else: - setup_celery_from_config(app_config, global_config) + if not asbool(app_config.get('celery_setup_elsewhere')): + if asbool(os.environ.get('CELERY_ALWAYS_EAGER')): + setup_celery_from_config( + app_config, global_config, + force_celery_always_eager=True) + else: + setup_celery_from_config(app_config, global_config) mgoblin_app = MediaGoblinApp( connection, db, diff --git a/mediagoblin/auth/forms.py b/mediagoblin/auth/forms.py index db8aaceb..7bc0aeb1 100644 --- a/mediagoblin/auth/forms.py +++ b/mediagoblin/auth/forms.py @@ -27,7 +27,9 @@ class RegistrationForm(wtforms.Form): 'Password', [wtforms.validators.Required(), wtforms.validators.Length(min=6, max=30), - wtforms.validators.EqualTo('confirm_password')]) + wtforms.validators.EqualTo( + 'confirm_password', + 'Passwords must match.')]) confirm_password = wtforms.PasswordField( 'Confirm password', [wtforms.validators.Required()]) diff --git a/mediagoblin/auth/lib.py b/mediagoblin/auth/lib.py index dc5f9941..f40e560f 100644 --- a/mediagoblin/auth/lib.py +++ b/mediagoblin/auth/lib.py @@ -19,7 +19,7 @@ import random import bcrypt -from mediagoblin.util import send_email +from mediagoblin.util import send_email, render_template from mediagoblin import globals as mgoblin_globals @@ -89,6 +89,10 @@ def fake_login_attempt(): randplus_stored_hash == randplus_hashed_pass +EMAIL_VERIFICATION_TEMPLATE = ( + u"http://{host}{uri}?" + u"userid={userid}&token={verification_key}") + def send_verification_email(user, request): """ Send the verification email to users to activate their accounts. @@ -97,9 +101,14 @@ def send_verification_email(user, request): - user: a user object - request: the request """ - - email_template = request.template_env.get_template( - 'mediagoblin/auth/verification_email.txt') + rendered_email = render_template( + request, 'mediagoblin/auth/verification_email.txt', + {'username': user['username'], + 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format( + host=request.host, + uri=request.urlgen('mediagoblin.auth.verify_email'), + userid=unicode(user['_id']), + verification_key=user['verification_key'])}) # TODO: There is no error handling in place send_email( @@ -111,10 +120,4 @@ def send_verification_email(user, request): # specific GNU MediaGoblin instance in the subject line. For # example "GNU MediaGoblin @ Wandborg - [...]". 'GNU MediaGoblin - Verify your email!', - email_template.render( - username=user['username'], - verification_url='http://{host}{uri}?userid={userid}&token={verification_key}'.format( - host=request.host, - uri=request.urlgen('mediagoblin.auth.verify_email'), - userid=unicode(user['_id']), - verification_key=user['verification_key']))) + rendered_email) diff --git a/mediagoblin/auth/routing.py b/mediagoblin/auth/routing.py index a8909fbb..46c585d2 100644 --- a/mediagoblin/auth/routing.py +++ b/mediagoblin/auth/routing.py @@ -20,7 +20,8 @@ auth_routes = [ Route('mediagoblin.auth.register', '/register/', controller='mediagoblin.auth.views:register'), Route('mediagoblin.auth.register_success', '/register/success/', - controller='mediagoblin.auth.views:register_success'), + template='mediagoblin/auth/register_success.html', + controller='mediagoblin.views:simple_template_render'), Route('mediagoblin.auth.login', '/login/', controller='mediagoblin.auth.views:login'), Route('mediagoblin.auth.logout', '/logout/', @@ -28,9 +29,11 @@ auth_routes = [ Route('mediagoblin.auth.verify_email', '/verify_email/', controller='mediagoblin.auth.views:verify_email'), Route('mediagoblin.auth.verify_email_notice', '/verification_required/', - controller='mediagoblin.auth.views:verify_email_notice'), + template='mediagoblin/auth/verification_needed.html', + controller='mediagoblin.views:simple_template_render'), Route('mediagoblin.auth.resend_verification', '/resend_verification/', controller='mediagoblin.auth.views:resend_activation'), Route('mediagoblin.auth.resend_verification_success', '/resend_verification_success/', - controller='mediagoblin.auth.views:resend_activation_success')] + template='mediagoblin/auth/resent_verification_email.html', + controller='mediagoblin.views:simple_template_render')] diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py index e4f1a7b1..1d00f382 100644 --- a/mediagoblin/auth/views.py +++ b/mediagoblin/auth/views.py @@ -16,8 +16,9 @@ import uuid -from webob import Response, exc +from webob import exc +from mediagoblin.util import render_to_response, redirect from mediagoblin.db.util import ObjectId from mediagoblin.auth import lib as auth_lib from mediagoblin.auth import forms as auth_forms @@ -53,25 +54,12 @@ def register(request): send_verification_email(entry, request) - # Redirect to register_success - return exc.HTTPFound( - location=request.urlgen("mediagoblin.auth.register_success")) + return redirect(request, "mediagoblin.auth.register_success") - # render - template = request.template_env.get_template( - 'mediagoblin/auth/register.html') - return Response( - template.render( - {'request': request, - 'register_form': register_form})) - - -def register_success(request): - template = request.template_env.get_template( - 'mediagoblin/auth/register_success.html') - return Response( - template.render( - {'request': request})) + return render_to_response( + request, + 'mediagoblin/auth/register.html', + {'register_form': register_form}) def login(request): @@ -96,8 +84,7 @@ def login(request): if request.POST.get('next'): return exc.HTTPFound(location=request.POST['next']) else: - return exc.HTTPFound( - location=request.urlgen("index")) + return redirect(request, "index") else: # Prevent detecting who's on this system by testing login @@ -105,23 +92,19 @@ def login(request): auth_lib.fake_login_attempt() login_failed = True - # render - template = request.template_env.get_template( - 'mediagoblin/auth/login.html') - return Response( - template.render( - {'request': request, - 'login_form': login_form, - 'next': request.GET.get('next') or request.POST.get('next'), - 'login_failed': login_failed})) + return render_to_response( + request, + 'mediagoblin/auth/login.html', + {'login_form': login_form, + 'next': request.GET.get('next') or request.POST.get('next'), + 'login_failed': login_failed}) def logout(request): # Maybe deleting the user_id parameter would be enough? request.session.delete() - return exc.HTTPFound( - location=request.urlgen("index")) + return redirect(request, "index") def verify_email(request): @@ -146,27 +129,11 @@ def verify_email(request): else: verification_successful = False - template = request.template_env.get_template( - 'mediagoblin/auth/verify_email.html') - return Response( - template.render( - {'request': request, - 'user': user, - 'verification_successful': verification_successful})) - -def verify_email_notice(request): - """ - Verify warning view. - - When the user tries to do some action that requires their account - to be verified beforehand, this view is called upon! - """ - - template = request.template_env.get_template( - 'mediagoblin/auth/verification_needed.html') - return Response( - template.render( - {'request': request})) + return render_to_response( + request, + 'mediagoblin/auth/verify_email.html', + {'user': user, + 'verification_successful': verification_successful}) def resend_activation(request): @@ -175,19 +142,9 @@ def resend_activation(request): Resend the activation email. """ - request.user['verification_key'] = unicode(uuid.uuid4()) request.user.save() send_verification_email(request.user, request) - return exc.HTTPFound( - location=request.urlgen('mediagoblin.auth.resend_verification_success')) - - -def resend_activation_success(request): - template = request.template_env.get_template( - 'mediagoblin/auth/resent_verification_email.html') - return Response( - template.render( - {'request': request})) + return redirect(request, 'mediagoblin.auth.resend_verification_success') diff --git a/mediagoblin/celery_setup/from_tests.py b/mediagoblin/celery_setup/from_tests.py new file mode 100644 index 00000000..fe7d7314 --- /dev/null +++ b/mediagoblin/celery_setup/from_tests.py @@ -0,0 +1,43 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from mediagoblin.tests.tools import TEST_APP_CONFIG +from mediagoblin import util +from mediagoblin.celery_setup import setup_celery_from_config +from mediagoblin.globals import setup_globals + + +OUR_MODULENAME = 'mediagoblin.celery_setup.from_tests' + + +def setup_self(setup_globals_func=setup_globals): + """ + Set up celery for testing's sake, which just needs to set up + celery and celery only. + """ + mgoblin_conf = util.read_config_file(TEST_APP_CONFIG) + mgoblin_section = mgoblin_conf['app:mediagoblin'] + + setup_celery_from_config( + mgoblin_section, mgoblin_conf, + settings_module=OUR_MODULENAME, + set_environ=False) + + +if os.environ.get('CELERY_CONFIG_MODULE') == OUR_MODULENAME: + setup_self() diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py index 34575320..c2fe3f9f 100644 --- a/mediagoblin/decorators.py +++ b/mediagoblin/decorators.py @@ -18,6 +18,7 @@ from bson.errors import InvalidId from webob import exc +from mediagoblin.util import redirect from mediagoblin.db.util import ObjectId @@ -38,9 +39,8 @@ def require_active_login(controller): def new_controller_func(request, *args, **kwargs): if request.user and \ request.user.get('status') == u'needs_email_verification': - return exc.HTTPFound( - location = request.urlgen( - 'mediagoblin.auth.verify_email_notice')) + return redirect(request, + 'mediagoblin.auth.verify_email_notice') elif not request.user or request.user.get('status') != u'active': return exc.HTTPFound( location="%s?next=%s" % ( diff --git a/mediagoblin/edit/routing.py b/mediagoblin/edit/routing.py index 54f2661a..bf0b2498 100644 --- a/mediagoblin/edit/routing.py +++ b/mediagoblin/edit/routing.py @@ -14,9 +14,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. + from routes.route import Route edit_routes = [ - Route('mediagoblin.edit.edit_media', "/{user}/{media}/", - controller="mediagoblin.edit.views:edit_media"), + # Media editing view handled in user_pages/routing.py ] diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index 5cfb2297..c5f0f435 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -15,8 +15,9 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. -from webob import Response, exc +from webob import exc +from mediagoblin.util import render_to_response, redirect from mediagoblin.edit import forms from mediagoblin.edit.lib import may_edit_media from mediagoblin.decorators import require_active_login, get_user_media_entry @@ -48,17 +49,13 @@ def edit_media(request, media): media['title'] = request.POST['title'] media['description'] = request.POST['description'] media['slug'] = request.POST['slug'] + media.save() - # redirect - return exc.HTTPFound( - location=request.urlgen("mediagoblin.user_pages.media_home", - user=media.uploader()['username'], media=media['_id'])) + return redirect(request, "mediagoblin.user_pages.media_home", + user=media.uploader()['username'], media=media['slug']) - # render - template = request.template_env.get_template( - 'mediagoblin/edit/edit.html') - return Response( - template.render( - {'request': request, - 'media': media, - 'form': form})) + return render_to_response( + request, + 'mediagoblin/edit/edit.html', + {'media': media, + 'form': form}) diff --git a/mediagoblin/submit/routing.py b/mediagoblin/submit/routing.py index cff28acb..3edbab70 100644 --- a/mediagoblin/submit/routing.py +++ b/mediagoblin/submit/routing.py @@ -20,5 +20,5 @@ submit_routes = [ Route('mediagoblin.submit.start', '/', controller='mediagoblin.submit.views:submit_start'), Route('mediagoblin.submit.success', '/success/', - controller='mediagoblin.submit.views:submit_success'), - ] + template='mediagoblin/submit/success.html', + controller='mediagoblin.views:simple_template_render')] diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index 262f2b12..b409b64d 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -17,9 +17,9 @@ from os.path import splitext from cgi import FieldStorage -from webob import Response, exc from werkzeug.utils import secure_filename +from mediagoblin.util import render_to_response, redirect from mediagoblin.decorators import require_active_login from mediagoblin.submit import forms as submit_forms from mediagoblin.process_media import process_media_initial @@ -75,23 +75,14 @@ def submit_start(request): # queue it for processing process_media_initial.delay(unicode(entry['_id'])) - # redirect - return exc.HTTPFound( - location=request.urlgen("mediagoblin.submit.success")) + return redirect(request, "mediagoblin.submit.success") - # render - template = request.template_env.get_template( - 'mediagoblin/submit/start.html') - return Response( - template.render( - {'request': request, - 'submit_form': submit_form})) + return render_to_response( + request, + 'mediagoblin/submit/start.html', + {'submit_form': submit_form}) def submit_success(request): - # render - template = request.template_env.get_template( - 'mediagoblin/submit/success.html') - return Response( - template.render( - {'request': request})) + return render_to_response( + request, 'mediagoblin/submit/success.html', {}) diff --git a/mediagoblin/templates/mediagoblin/auth/login.html b/mediagoblin/templates/mediagoblin/auth/login.html index 22a57b70..c2e27c15 100644 --- a/mediagoblin/templates/mediagoblin/auth/login.html +++ b/mediagoblin/templates/mediagoblin/auth/login.html @@ -33,7 +33,8 @@ <input type="submit" value="submit" class="button"/> </div> {% if next %} - <input type="hidden" name="next" value="{{ next }}" class="button" /> + <input type="hidden" name="next" value="{{ next }}" class="button" + style="display: none;"/> {% endif %} <p>Don't have an account yet? <a href="{{ request.urlgen('mediagoblin.auth.register') }}">Create one here!</a></p> </div> diff --git a/mediagoblin/tests/mgoblin_test_app.ini b/mediagoblin/tests/mgoblin_test_app.ini new file mode 100644 index 00000000..abed2615 --- /dev/null +++ b/mediagoblin/tests/mgoblin_test_app.ini @@ -0,0 +1,46 @@ +[DEFAULT] +debug = true + +[composite:main] +use = egg:Paste#urlmap +/ = mediagoblin +/mgoblin_media/ = publicstore_serve +/mgoblin_static/ = mediagoblin_static + +[app:mediagoblin] +use = egg:mediagoblin#app +filter-with = beaker +queuestore_base_dir = %(here)s/test_user_dev/media/queue +publicstore_base_dir = %(here)s/test_user_dev/media/public +publicstore_base_url = /mgoblin_media/ +direct_remote_path = /mgoblin_static/ +email_sender_address = "notice@mediagoblin.example.org" +email_debug_mode = true +db_name = __mediagoblin_tests__ +# Celery shouldn't be set up by the paste app factory as it's set up +# elsewhere +celery_setup_elsewhere = true + +[app:publicstore_serve] +use = egg:Paste#static +document_root = %(here)s/user_dev/media/public + +[app:mediagoblin_static] +use = egg:Paste#static +document_root = %(here)s/mediagoblin/static/ + +[filter:beaker] +use = egg:Beaker#beaker_session +cache_dir = %(here)s/test_user_dev/beaker +beaker.session.key = mediagoblin +# beaker.session.secret = somesupersecret +beaker.session.data_dir = %(here)s/test_user_dev/beaker/sessions/data +beaker.session.lock_dir = %(here)s/test_user_dev/beaker/sessions/lock + +[celery] +celery_always_eager = true + +[server:main] +use = egg:Paste#http +host = 127.0.0.1 +port = 6543 diff --git a/mediagoblin/tests/test_auth.py b/mediagoblin/tests/test_auth.py index 94ce6bba..cdfeccab 100644 --- a/mediagoblin/tests/test_auth.py +++ b/mediagoblin/tests/test_auth.py @@ -14,8 +14,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import urlparse + +from nose.tools import assert_equal from mediagoblin.auth import lib as auth_lib +from mediagoblin.tests.tools import setup_fresh_app +from mediagoblin import globals as mgoblin_globals +from mediagoblin import util ######################## @@ -57,3 +63,169 @@ def test_bcrypt_gen_password_hash(): pw, hashed_pw, '3><7R45417') assert not auth_lib.bcrypt_check_password( 'notthepassword', hashed_pw, '3><7R45417') + + +@setup_fresh_app +def test_register_views(test_app): + """ + Massive test function that all our registration-related views all work. + """ + # Test doing a simple GET on the page + # ----------------------------------- + + test_app.get('/auth/register/') + # Make sure it rendered with the appropriate template + assert util.TEMPLATE_TEST_CONTEXT.has_key( + 'mediagoblin/auth/register.html') + + # Try to register without providing anything, should error + # -------------------------------------------------------- + + util.clear_test_template_context() + test_app.post( + '/auth/register/', {}) + context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + form = context['register_form'] + assert form.username.errors == [u'This field is required.'] + assert form.password.errors == [u'This field is required.'] + assert form.confirm_password.errors == [u'This field is required.'] + assert form.email.errors == [u'This field is required.'] + + # Try to register with fields that are known to be invalid + # -------------------------------------------------------- + + ## too short + util.clear_test_template_context() + test_app.post( + '/auth/register/', { + 'username': 'l', + 'password': 'o', + 'confirm_password': 'o', + 'email': 'l'}) + context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + form = context['register_form'] + + assert form.username.errors == [ + u'Field must be between 3 and 30 characters long.'] + assert form.password.errors == [ + u'Field must be between 6 and 30 characters long.'] + + ## bad form + util.clear_test_template_context() + test_app.post( + '/auth/register/', { + 'username': '@_@', + 'email': 'lollerskates'}) + context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + form = context['register_form'] + + assert form.username.errors == [ + u'Invalid input.'] + assert form.email.errors == [ + u'Invalid email address.'] + + ## mismatching passwords + util.clear_test_template_context() + test_app.post( + '/auth/register/', { + 'password': 'herpderp', + 'confirm_password': 'derpherp'}) + context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + form = context['register_form'] + + assert form.password.errors == [ + u'Passwords must match.'] + + ## At this point there should be no users in the database ;) + assert not mgoblin_globals.database.User.find().count() + + # Successful register + # ------------------- + util.clear_test_template_context() + response = test_app.post( + '/auth/register/', { + 'username': 'happygirl', + 'password': 'iamsohappy', + 'confirm_password': 'iamsohappy', + 'email': 'happygrrl@example.org'}) + response.follow() + + ## Did we redirect to the proper page? Use the right template? + assert_equal( + urlparse.urlsplit(response.location)[2], + '/auth/register/success/') + assert util.TEMPLATE_TEST_CONTEXT.has_key( + 'mediagoblin/auth/register_success.html') + + ## Make sure user is in place + new_user = mgoblin_globals.database.User.find_one( + {'username': 'happygirl'}) + assert new_user + assert new_user['status'] == u'needs_email_verification' + assert new_user['email_verified'] == False + + ## Make sure we get email confirmation, and try verifying + assert len(util.EMAIL_TEST_INBOX) == 1 + message = util.EMAIL_TEST_INBOX.pop() + assert message['To'] == 'happygrrl@example.org' + email_context = util.TEMPLATE_TEST_CONTEXT[ + 'mediagoblin/auth/verification_email.txt'] + assert email_context['verification_url'] in message.get_payload(decode=True) + + path = urlparse.urlsplit(email_context['verification_url'])[2] + get_params = urlparse.urlsplit(email_context['verification_url'])[3] + assert path == u'/auth/verify_email/' + parsed_get_params = urlparse.parse_qs(get_params) + + ### user should have these same parameters + assert parsed_get_params['userid'] == [ + unicode(new_user['_id'])] + assert parsed_get_params['token'] == [ + new_user['verification_key']] + + ## Try verifying with bs verification key, shouldn't work + util.clear_test_template_context() + test_app.get( + "/auth/verify_email/?userid=%s&token=total_bs" % unicode( + new_user['_id'])) + context = util.TEMPLATE_TEST_CONTEXT[ + 'mediagoblin/auth/verify_email.html'] + assert context['verification_successful'] == False + new_user = mgoblin_globals.database.User.find_one( + {'username': 'happygirl'}) + assert new_user + assert new_user['status'] == u'needs_email_verification' + assert new_user['email_verified'] == False + + ## Verify the email activation works + util.clear_test_template_context() + test_app.get("%s?%s" % (path, get_params)) + context = util.TEMPLATE_TEST_CONTEXT[ + 'mediagoblin/auth/verify_email.html'] + assert context['verification_successful'] == True + new_user = mgoblin_globals.database.User.find_one( + {'username': 'happygirl'}) + assert new_user + assert new_user['status'] == u'active' + assert new_user['email_verified'] == True + + ## TODO: Try logging in + + # Uniqueness checks + # ----------------- + ## We shouldn't be able to register with that user twice + util.clear_test_template_context() + response = test_app.post( + '/auth/register/', { + 'username': 'happygirl', + 'password': 'iamsohappy2', + 'confirm_password': 'iamsohappy2', + 'email': 'happygrrl2@example.org'}) + + context = util.TEMPLATE_TEST_CONTEXT[ + 'mediagoblin/auth/register.html'] + form = context['register_form'] + assert form.username.errors == [ + u'Sorry, a user with that name already exists.'] + + ## TODO: Also check for double instances of an email address? diff --git a/mediagoblin/tests/test_tests.py b/mediagoblin/tests/test_tests.py new file mode 100644 index 00000000..3ecbfac7 --- /dev/null +++ b/mediagoblin/tests/test_tests.py @@ -0,0 +1,38 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from mediagoblin.tests.tools import get_test_app + +from mediagoblin import globals as mgoblin_globals + + +def test_get_test_app_wipes_db(): + """ + Make sure we get a fresh database on every wipe :) + """ + get_test_app() + assert mgoblin_globals.database.User.find().count() == 0 + + new_user = mgoblin_globals.database.User() + new_user['username'] = u'lolcat' + new_user['email'] = u'lol@cats.example.org' + new_user['pw_hash'] = u'pretend_this_is_a_hash' + new_user.save() + assert mgoblin_globals.database.User.find().count() == 1 + + get_test_app() + + assert mgoblin_globals.database.User.find().count() == 0 diff --git a/mediagoblin/tests/tools.py b/mediagoblin/tests/tools.py new file mode 100644 index 00000000..342b54b7 --- /dev/null +++ b/mediagoblin/tests/tools.py @@ -0,0 +1,109 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import pkg_resources +import os, shutil + +from paste.deploy import appconfig, loadapp +from webtest import TestApp + +from mediagoblin import util +from mediagoblin.decorators import _make_safe +from mediagoblin.db.open import setup_connection_and_db_from_config + + +MEDIAGOBLIN_TEST_DB_NAME = '__mediagoblinunittests__' +TEST_APP_CONFIG = pkg_resources.resource_filename( + 'mediagoblin.tests', 'mgoblin_test_app.ini') +TEST_USER_DEV = pkg_resources.resource_filename( + 'mediagoblin.tests', 'test_user_dev') +MGOBLIN_APP = None + +USER_DEV_DIRECTORIES_TO_SETUP = [ + 'media/public', 'media/queue', + 'beaker/sessions/data', 'beaker/sessions/lock'] + + +class BadCeleryEnviron(Exception): pass + + +def get_test_app(dump_old_app=True): + if not os.environ.get('CELERY_CONFIG_MODULE') == \ + 'mediagoblin.celery_setup.from_tests': + raise BadCeleryEnviron( + u"Sorry, you *absolutely* must run nosetests with the\n" + u"mediagoblin.celery_setup.from_tests module. Like so:\n" + u"$ CELERY_CONFIG_MODULE=mediagoblin.celery_setup.from_tests ./bin/nosetests") + + # Just return the old app if that exists and it's okay to set up + # and return + if MGOBLIN_APP and not dump_old_app: + return MGOBLIN_APP + + # Remove and reinstall user_dev directories + if os.path.exists(TEST_USER_DEV): + shutil.rmtree(TEST_USER_DEV) + + for directory in USER_DEV_DIRECTORIES_TO_SETUP: + full_dir = os.path.join(TEST_USER_DEV, directory) + os.makedirs(full_dir) + + # Get app config + config = appconfig( + 'config:' + os.path.basename(TEST_APP_CONFIG), + relative_to=os.path.dirname(TEST_APP_CONFIG), + name='mediagoblin') + + # Wipe database + # @@: For now we're dropping collections, but we could also just + # collection.remove() ? + connection, db = setup_connection_and_db_from_config( + config.local_conf) + + collections_to_wipe = [ + collection + for collection in db.collection_names() + if not collection.startswith('system.')] + + for collection in collections_to_wipe: + db.drop_collection(collection) + + # Don't need these anymore... + del(connection) + del(db) + + # TODO: Drop and recreate indexes + + # setup app and return + test_app = loadapp( + 'config:' + TEST_APP_CONFIG) + + return TestApp(test_app) + + +def setup_fresh_app(func): + """ + Decorator to setup a fresh test application for this function. + + Cleans out test buckets and passes in a new, fresh test_app. + """ + def wrapper(*args, **kwargs): + test_app = get_test_app() + util.clear_test_buckets() + return func(test_app, *args, **kwargs) + + return _make_safe(wrapper, func) diff --git a/mediagoblin/user_pages/routing.py b/mediagoblin/user_pages/routing.py index 96f97427..c5e9a984 100644 --- a/mediagoblin/user_pages/routing.py +++ b/mediagoblin/user_pages/routing.py @@ -22,5 +22,7 @@ user_routes = [ Route('mediagoblin.user_pages.media_home', '/{user}/m/{media}/', requirements=dict(m_id="[0-9a-fA-F]{24}"), controller="mediagoblin.user_pages.views:media_home"), + Route('mediagoblin.edit.edit_media', "/{user}/m/{media}/edit/", + controller="mediagoblin.edit.views:edit_media"), Route('mediagoblin.user_pages.atom_feed', '/{user}/atom/', controller="mediagoblin.user_pages.views:atom_feed")] diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index cc9c7b21..323c3e54 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -14,9 +14,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from webob import Response, exc +from webob import exc from mediagoblin.db.util import DESCENDING -from mediagoblin.util import Pagination +from mediagoblin.util import Pagination, render_to_response from mediagoblin.decorators import uses_pagination, get_user_media_entry @@ -42,26 +42,22 @@ def user_home(request, page): if media_entries == None: return exc.HTTPNotFound() - template = request.template_env.get_template( - 'mediagoblin/user_pages/user.html') - - return Response( - template.render( - {'request': request, - 'user': user, - 'media_entries': media_entries, - 'pagination': pagination})) + return render_to_response( + request, + 'mediagoblin/user_pages/user.html', + {'user': user, + 'media_entries': media_entries, + 'pagination': pagination}) @get_user_media_entry def media_home(request, media): """'Homepage' of a MediaEntry()""" - template = request.template_env.get_template( - 'mediagoblin/user_pages/media.html') - return Response( - template.render( - {'request': request, - 'media': media})) + return render_to_response( + request, + 'mediagoblin/user_pages/media.html', + {'media': media}) + ATOM_DEFAULT_NR_OF_UPDATED_ITEMS = 5 diff --git a/mediagoblin/util.py b/mediagoblin/util.py index 2865cf11..64e21ca9 100644 --- a/mediagoblin/util.py +++ b/mediagoblin/util.py @@ -18,17 +18,22 @@ from email.MIMEText import MIMEText import gettext import pkg_resources import smtplib +import os import sys import re +import urllib +from math import ceil +import copy + +from babel.localedata import exists import jinja2 -from mediagoblin.db.util import ObjectId import translitcodec +from paste.deploy.loadwsgi import NicerConfigParser +from webob import Response, exc from mediagoblin import globals as mgoblin_globals +from mediagoblin.db.util import ObjectId -import urllib -from math import ceil -import copy TESTS_ENABLED = False def _activate_testing(): @@ -39,6 +44,26 @@ def _activate_testing(): TESTS_ENABLED = True +def clear_test_buckets(): + """ + We store some things for testing purposes that should be cleared + when we want a "clean slate" of information for our next round of + tests. Call this function to wipe all that stuff clean. + + Also wipes out some other things we might redefine during testing, + like the jinja envs. + """ + global SETUP_JINJA_ENVS + SETUP_JINJA_ENVS = {} + + global EMAIL_TEST_INBOX + global EMAIL_TEST_MBOX_INBOX + EMAIL_TEST_INBOX = [] + EMAIL_TEST_MBOX_INBOX = [] + + clear_test_template_context() + + def get_jinja_loader(user_template_path=None): """ Set up the Jinja template loaders, possibly allowing for user @@ -55,6 +80,9 @@ def get_jinja_loader(user_template_path=None): return jinja2.PackageLoader('mediagoblin', 'templates') +SETUP_JINJA_ENVS = {} + + def get_jinja_env(template_loader, locale): """ Set up the Jinja environment, @@ -64,6 +92,11 @@ def get_jinja_env(template_loader, locale): """ setup_gettext(locale) + # If we have a jinja environment set up with this locale, just + # return that one. + if SETUP_JINJA_ENVS.has_key(locale): + return SETUP_JINJA_ENVS[locale] + template_env = jinja2.Environment( loader=template_loader, autoescape=True, extensions=['jinja2.ext.i18n']) @@ -72,9 +105,49 @@ def get_jinja_env(template_loader, locale): mgoblin_globals.translations.gettext, mgoblin_globals.translations.ngettext) + if exists(locale): + SETUP_JINJA_ENVS[locale] = template_env + return template_env +# We'll store context information here when doing unit tests +TEMPLATE_TEST_CONTEXT = {} + + +def render_template(request, template_path, context): + """ + Render a template with context. + + Always inserts the request into the context, so you don't have to. + Also stores the context if we're doing unit tests. Helpful! + """ + template = request.template_env.get_template( + template_path) + context['request'] = request + rendered = template.render(context) + + if TESTS_ENABLED: + TEMPLATE_TEST_CONTEXT[template_path] = context + + return rendered + + +def clear_test_template_context(): + global TEMPLATE_TEST_CONTEXT + TEMPLATE_TEST_CONTEXT = {} + + +def render_to_response(request, template, context): + """Much like Django's shortcut.render()""" + return Response(render_template(request, template, context)) + + +def redirect(request, *args, **kwargs): + """Returns a HTTPFound(), takes a request and then urlgen params""" + return exc.HTTPFound(location=request.urlgen(*args, **kwargs)) + + def setup_user_in_request(request): """ Examine a request and tack on a request.user parameter if that's @@ -278,6 +351,30 @@ def get_locale_from_request(request): return locale_to_lower_upper(target_lang) +def read_config_file(conf_file): + """ + Read a paste deploy style config file and process it. + """ + if not os.path.exists(conf_file): + raise IOError( + "MEDIAGOBLIN_CONFIG not set or file does not exist") + + parser = NicerConfigParser(conf_file) + parser.read(conf_file) + parser._defaults.setdefault( + 'here', os.path.dirname(os.path.abspath(conf_file))) + parser._defaults.setdefault( + '__file__', os.path.abspath(conf_file)) + + mgoblin_conf = dict( + [(section_name, dict(parser.items(section_name))) + for section_name in parser.sections()]) + + return mgoblin_conf + + +SETUP_GETTEXTS = {} + def setup_gettext(locale): """ Setup the gettext instance based on this locale @@ -288,8 +385,13 @@ def setup_gettext(locale): # TODO: fallback nicely on translations from pt_PT to pt if not # available, etc. - this_gettext = gettext.translation( - 'mediagoblin', TRANSLATIONS_PATH, [locale], fallback=True) + if SETUP_GETTEXTS.has_key(locale): + this_gettext = SETUP_GETTEXTS[locale] + else: + this_gettext = gettext.translation( + 'mediagoblin', TRANSLATIONS_PATH, [locale], fallback=True) + if exists(locale): + SETUP_GETTEXTS[locale] = this_gettext mgoblin_globals.setup_globals( translations=this_gettext) diff --git a/mediagoblin/views.py b/mediagoblin/views.py index dd722c63..5b6d9773 100644 --- a/mediagoblin/views.py +++ b/mediagoblin/views.py @@ -14,16 +14,23 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from webob import Response +from mediagoblin.util import render_to_response from mediagoblin.db.util import DESCENDING def root_view(request): media_entries = request.db.MediaEntry.find( {u'state': u'processed'}).sort('created', DESCENDING) - template = request.template_env.get_template( - 'mediagoblin/root.html') - return Response( - template.render( - {'request': request, - 'media_entries': media_entries})) + return render_to_response( + request, 'mediagoblin/root.html', + {'media_entries': media_entries}) + + +def simple_template_render(request): + """ + A view for absolutely simple template rendering. + Just make sure 'template' is in the matchdict! + """ + template_name = request.matchdict['template'] + return render_to_response( + request, template_name, {}) @@ -41,6 +41,7 @@ setup( 'Babel', 'translitcodec', 'argparse', + 'webtest', ], test_suite='nose.collector', |