Commit 257dae63 by Calen Pennington

Merge pull request #2090 from edx/fix/cale/amazon-ses-errors

Fix/cale/amazon ses errors
parents 9e542aa5 f521b8e8
from student.models import (User, UserProfile, Registration, from student.models import (User, UserProfile, Registration,
CourseEnrollmentAllowed, CourseEnrollment) CourseEnrollmentAllowed, CourseEnrollment,
PendingEmailChange)
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from datetime import datetime from datetime import datetime
from factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall, post_generation from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence
from uuid import uuid4 from uuid import uuid4
# Factories don't have __init__ methods, and are self documenting
# pylint: disable=W0232
class GroupFactory(DjangoModelFactory): class GroupFactory(DjangoModelFactory):
FACTORY_FOR = Group FACTORY_FOR = Group
name = 'staff_MITx/999/Robot_Super_Course' name = u'staff_MITx/999/Robot_Super_Course'
class UserProfileFactory(DjangoModelFactory): class UserProfileFactory(DjangoModelFactory):
FACTORY_FOR = UserProfile FACTORY_FOR = UserProfile
user = None user = None
name = 'Robot Test' name = u'Robot Test'
level_of_education = None level_of_education = None
gender = 'm' gender = u'm'
mailing_address = None mailing_address = None
goals = 'World domination' goals = u'World domination'
class RegistrationFactory(DjangoModelFactory): class RegistrationFactory(DjangoModelFactory):
FACTORY_FOR = Registration FACTORY_FOR = Registration
user = None user = None
activation_key = uuid4().hex activation_key = uuid4().hex.decode('ascii')
class UserFactory(DjangoModelFactory): class UserFactory(DjangoModelFactory):
FACTORY_FOR = User FACTORY_FOR = User
username = 'robot' username = Sequence(u'robot{0}'.format)
email = 'robot+test@edx.org' email = Sequence(u'robot+test+{0}@edx.org'.format)
password = PostGenerationMethodCall('set_password', password = PostGenerationMethodCall('set_password',
'test') 'test')
first_name = 'Robot' first_name = Sequence(u'Robot{0}'.format)
last_name = 'Test' last_name = 'Test'
is_staff = False is_staff = False
is_active = True is_active = True
...@@ -64,7 +68,7 @@ class CourseEnrollmentFactory(DjangoModelFactory): ...@@ -64,7 +68,7 @@ class CourseEnrollmentFactory(DjangoModelFactory):
FACTORY_FOR = CourseEnrollment FACTORY_FOR = CourseEnrollment
user = SubFactory(UserFactory) user = SubFactory(UserFactory)
course_id = 'edX/toy/2012_Fall' course_id = u'edX/toy/2012_Fall'
class CourseEnrollmentAllowedFactory(DjangoModelFactory): class CourseEnrollmentAllowedFactory(DjangoModelFactory):
...@@ -72,3 +76,17 @@ class CourseEnrollmentAllowedFactory(DjangoModelFactory): ...@@ -72,3 +76,17 @@ class CourseEnrollmentAllowedFactory(DjangoModelFactory):
email = 'test@edx.org' email = 'test@edx.org'
course_id = 'edX/test/2012_Fall' course_id = 'edX/test/2012_Fall'
class PendingEmailChangeFactory(DjangoModelFactory):
"""Factory for PendingEmailChange objects
user: generated by UserFactory
new_email: sequence of new+email+{}@edx.org
activation_key: sequence of integers, padded to 30 characters
"""
FACTORY_FOR = PendingEmailChange
user = SubFactory(UserFactory)
new_email = Sequence(u'new+email+{0}@edx.org'.format)
activation_key = Sequence(u'{:0<30d}'.format)
import json
import django.db
from student.tests.factories import UserFactory, RegistrationFactory, PendingEmailChangeFactory
from student.views import reactivation_email_for_user, change_email_request, confirm_email_change
from student.models import UserProfile, PendingEmailChange
from django.contrib.auth.models import User
from django.test import TestCase, TransactionTestCase
from django.test.client import RequestFactory
from mock import Mock, patch
from django.http import Http404, HttpResponse
from django.conf import settings
from nose.plugins.skip import SkipTest
class TestException(Exception):
"""Exception used for testing that nothing will catch explicitly"""
pass
def mock_render_to_string(template_name, context):
"""Return a string that encodes template_name and context"""
return str((template_name, sorted(context.iteritems())))
def mock_render_to_response(template_name, context):
"""Return an HttpResponse with content that encodes template_name and context"""
return HttpResponse(mock_render_to_string(template_name, context))
class EmailTestMixin(object):
"""Adds useful assertions for testing `email_user`"""
def assertEmailUser(self, email_user, subject_template, subject_context, body_template, body_context):
"""Assert that `email_user` was used to send and email with the supplied subject and body
`email_user`: The mock `django.contrib.auth.models.User.email_user` function
to verify
`subject_template`: The template to have been used for the subject
`subject_context`: The context to have been used for the subject
`body_template`: The template to have been used for the body
`body_context`: The context to have been used for the body
"""
email_user.assert_called_with(
mock_render_to_string(subject_template, subject_context),
mock_render_to_string(body_template, body_context),
settings.DEFAULT_FROM_EMAIL
)
@patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True))
@patch('django.contrib.auth.models.User.email_user')
class ReactivationEmailTests(EmailTestMixin, TestCase):
"""Test sending a reactivation email to a user"""
def setUp(self):
self.user = UserFactory.create()
self.registration = RegistrationFactory.create(user=self.user)
def reactivation_email(self):
"""Send the reactivation email, and return the response as json data"""
return json.loads(reactivation_email_for_user(self.user).content)
def assertReactivateEmailSent(self, email_user):
"""Assert that the correct reactivation email has been sent"""
context = {
'name': self.user.profile.name,
'key': self.registration.activation_key
}
self.assertEmailUser(
email_user,
'emails/activation_email_subject.txt',
context,
'emails/activation_email.txt',
context
)
def test_reactivation_email_failure(self, email_user):
self.user.email_user.side_effect = Exception
response_data = self.reactivation_email()
self.assertReactivateEmailSent(email_user)
self.assertFalse(response_data['success'])
def test_reactivation_email_success(self, email_user):
response_data = self.reactivation_email()
self.assertReactivateEmailSent(email_user)
self.assertTrue(response_data['success'])
class EmailChangeRequestTests(TestCase):
"""Test changing a user's email address"""
def setUp(self):
self.user = UserFactory.create()
self.new_email = 'new.email@edx.org'
self.req_factory = RequestFactory()
self.request = self.req_factory.post('unused_url', data={
'password': 'test',
'new_email': self.new_email
})
self.request.user = self.user
self.user.email_user = Mock()
def run_request(self, request=None):
"""Execute request and return result parsed as json
If request isn't passed in, use self.request instead
"""
if request is None:
request = self.request
response = change_email_request(self.request)
return json.loads(response.content)
def assertFailedRequest(self, response_data, expected_error):
"""Assert that `response_data` indicates a failed request that returns `expected_error`"""
self.assertFalse(response_data['success'])
self.assertEquals(expected_error, response_data['error'])
self.assertFalse(self.user.email_user.called)
def test_unauthenticated(self):
self.user.is_authenticated = False
with self.assertRaises(Http404):
change_email_request(self.request)
self.assertFalse(self.user.email_user.called)
def test_invalid_password(self):
self.request.POST['password'] = 'wrong'
self.assertFailedRequest(self.run_request(), 'Invalid password')
def test_invalid_emails(self):
for email in ('bad_email', 'bad_email@', '@bad_email'):
self.request.POST['new_email'] = email
self.assertFailedRequest(self.run_request(), 'Valid e-mail address required.')
def check_duplicate_email(self, email):
"""Test that a request to change a users email to `email` fails"""
request = self.req_factory.post('unused_url', data={
'new_email': email,
'password': 'test',
})
request.user = self.user
self.assertFailedRequest(self.run_request(request), 'An account with this e-mail already exists.')
def test_duplicate_email(self):
UserFactory.create(email=self.new_email)
self.check_duplicate_email(self.new_email)
def test_capitalized_duplicate_email(self):
raise SkipTest("We currently don't check for emails in a case insensitive way, but we should")
UserFactory.create(email=self.new_email)
self.check_duplicate_email(self.new_email.capitalize())
# TODO: Finish testing the rest of change_email_request
@patch('django.contrib.auth.models.User.email_user')
@patch('student.views.render_to_response', Mock(side_effect=mock_render_to_response, autospec=True))
@patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True))
class EmailChangeConfirmationTests(EmailTestMixin, TransactionTestCase):
"""Test that confirmation of email change requests function even in the face of exceptions thrown while sending email"""
def setUp(self):
self.user = UserFactory.create()
self.profile = UserProfile.objects.get(user=self.user)
self.req_factory = RequestFactory()
self.request = self.req_factory.get('unused_url')
self.request.user = self.user
self.user.email_user = Mock()
self.pending_change_request = PendingEmailChangeFactory.create(user=self.user)
self.key = self.pending_change_request.activation_key
def assertRolledBack(self):
"""Assert that no changes to user, profile, or pending email have been made to the db"""
self.assertEquals(self.user.email, User.objects.get(username=self.user.username).email)
self.assertEquals(self.profile.meta, UserProfile.objects.get(user=self.user).meta)
self.assertEquals(1, PendingEmailChange.objects.count())
def assertFailedBeforeEmailing(self, email_user):
"""Assert that the function failed before emailing a user"""
self.assertRolledBack()
self.assertFalse(email_user.called)
def check_confirm_email_change(self, expected_template, expected_context):
"""Call `confirm_email_change` and assert that the content was generated as expected
`expected_template`: The name of the template that should have been used
to generate the content
`expected_context`: The context dictionary that should have been used to
generate the content
"""
response = confirm_email_change(self.request, self.key)
self.assertEquals(
mock_render_to_response(expected_template, expected_context).content,
response.content
)
def assertChangeEmailSent(self, email_user):
"""Assert that the correct email was sent to confirm an email change"""
context = {
'old_email': self.user.email,
'new_email': self.pending_change_request.new_email,
}
self.assertEmailUser(
email_user,
'emails/email_change_subject.txt',
context,
'emails/confirm_email_change.txt',
context
)
def test_not_pending(self, email_user):
self.key = 'not_a_key'
self.check_confirm_email_change('invalid_email_key.html', {})
self.assertFailedBeforeEmailing(email_user)
def test_duplicate_email(self, email_user):
UserFactory.create(email=self.pending_change_request.new_email)
self.check_confirm_email_change('email_exists.html', {})
self.assertFailedBeforeEmailing(email_user)
def test_old_email_fails(self, email_user):
email_user.side_effect = [Exception, None]
self.check_confirm_email_change('email_change_failed.html', {
'email': self.user.email,
})
self.assertRolledBack()
self.assertChangeEmailSent(email_user)
def test_new_email_fails(self, email_user):
email_user.side_effect = [None, Exception]
self.check_confirm_email_change('email_change_failed.html', {
'email': self.pending_change_request.new_email
})
self.assertRolledBack()
self.assertChangeEmailSent(email_user)
def test_successful_email_change(self, email_user):
self.check_confirm_email_change('email_change_successful.html', {
'old_email': self.user.email,
'new_email': self.pending_change_request.new_email
})
self.assertChangeEmailSent(email_user)
meta = json.loads(UserProfile.objects.get(user=self.user).meta)
self.assertIn('old_emails', meta)
self.assertEquals(self.user.email, meta['old_emails'][0][0])
self.assertEquals(
self.pending_change_request.new_email,
User.objects.get(username=self.user.username).email
)
self.assertEquals(0, PendingEmailChange.objects.count())
@patch('student.views.PendingEmailChange.objects.get', Mock(side_effect=TestException))
@patch('student.views.transaction.rollback', wraps=django.db.transaction.rollback)
def test_always_rollback(self, rollback, _email_user):
with self.assertRaises(TestException):
confirm_email_change(self.request, self.key)
rollback.assert_called_with()
...@@ -19,7 +19,7 @@ from django.core.context_processors import csrf ...@@ -19,7 +19,7 @@ from django.core.context_processors import csrf
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.validators import validate_email, validate_slug, ValidationError from django.core.validators import validate_email, validate_slug, ValidationError
from django.db import IntegrityError from django.db import IntegrityError, transaction
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed, HttpResponseRedirect, Http404 from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed, HttpResponseRedirect, Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django_future.csrf import ensure_csrf_cookie, csrf_exempt from django_future.csrf import ensure_csrf_cookie, csrf_exempt
...@@ -655,7 +655,7 @@ def create_account(request, post_override=None): ...@@ -655,7 +655,7 @@ def create_account(request, post_override=None):
elif not settings.GENERATE_RANDOM_USER_CREDENTIALS: elif not settings.GENERATE_RANDOM_USER_CREDENTIALS:
res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
except: except:
log.exception(sys.exc_info()) log.warning('Unable to send activation email to user', exc_info=True)
js['value'] = 'Could not send activation e-mail.' js['value'] = 'Could not send activation e-mail.'
return HttpResponse(json.dumps(js)) return HttpResponse(json.dumps(js))
...@@ -975,7 +975,11 @@ def reactivation_email_for_user(user): ...@@ -975,7 +975,11 @@ def reactivation_email_for_user(user):
subject = ''.join(subject.splitlines()) subject = ''.join(subject.splitlines())
message = render_to_string('emails/activation_email.txt', d) message = render_to_string('emails/activation_email.txt', d)
res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) try:
res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
except:
log.warning('Unable to send reactivation email', exc_info=True)
return HttpResponse(json.dumps({'success': False, 'error': 'Unable to send reactivation email'}))
return HttpResponse(json.dumps({'success': True})) return HttpResponse(json.dumps({'success': True}))
...@@ -1001,7 +1005,7 @@ def change_email_request(request): ...@@ -1001,7 +1005,7 @@ def change_email_request(request):
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'error': 'Valid e-mail address required.'})) 'error': 'Valid e-mail address required.'}))
if len(User.objects.filter(email=new_email)) != 0: if User.objects.filter(email=new_email).count() != 0:
## CRITICAL TODO: Handle case sensitivity for e-mails ## CRITICAL TODO: Handle case sensitivity for e-mails
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'error': 'An account with this e-mail already exists.'})) 'error': 'An account with this e-mail already exists.'}))
...@@ -1036,41 +1040,63 @@ def change_email_request(request): ...@@ -1036,41 +1040,63 @@ def change_email_request(request):
@ensure_csrf_cookie @ensure_csrf_cookie
@transaction.commit_manually
def confirm_email_change(request, key): def confirm_email_change(request, key):
''' User requested a new e-mail. This is called when the activation ''' User requested a new e-mail. This is called when the activation
link is clicked. We confirm with the old e-mail, and update link is clicked. We confirm with the old e-mail, and update
''' '''
try: try:
pec = PendingEmailChange.objects.get(activation_key=key) try:
except PendingEmailChange.DoesNotExist: pec = PendingEmailChange.objects.get(activation_key=key)
return render_to_response("invalid_email_key.html", {}) except PendingEmailChange.DoesNotExist:
transaction.rollback()
user = pec.user return render_to_response("invalid_email_key.html", {})
d = {'old_email': user.email,
'new_email': pec.new_email} user = pec.user
address_context = {
'old_email': user.email,
'new_email': pec.new_email
}
if len(User.objects.filter(email=pec.new_email)) != 0: if len(User.objects.filter(email=pec.new_email)) != 0:
return render_to_response("email_exists.html", d) transaction.rollback()
return render_to_response("email_exists.html", {})
subject = render_to_string('emails/email_change_subject.txt', address_context)
subject = ''.join(subject.splitlines())
message = render_to_string('emails/confirm_email_change.txt', address_context)
up = UserProfile.objects.get(user=user)
meta = up.get_meta()
if 'old_emails' not in meta:
meta['old_emails'] = []
meta['old_emails'].append([user.email, datetime.datetime.now().isoformat()])
up.set_meta(meta)
up.save()
# Send it to the old email...
try:
user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
except Exception:
transaction.rollback()
log.warning('Unable to send confirmation email to old address', exc_info=True)
return render_to_response("email_change_failed.html", {'email': user.email})
subject = render_to_string('emails/email_change_subject.txt', d) user.email = pec.new_email
subject = ''.join(subject.splitlines()) user.save()
message = render_to_string('emails/confirm_email_change.txt', d) pec.delete()
up = UserProfile.objects.get(user=user) # And send it to the new email...
meta = up.get_meta() try:
if 'old_emails' not in meta: user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
meta['old_emails'] = [] except Exception:
meta['old_emails'].append([user.email, datetime.datetime.now().isoformat()]) transaction.rollback()
up.set_meta(meta) log.warning('Unable to send confirmation email to new address', exc_info=True)
up.save() return render_to_response("email_change_failed.html", {'email': pec.new_email})
# Send it to the old email...
user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) transaction.commit()
user.email = pec.new_email return render_to_response("email_change_successful.html", address_context)
user.save() except Exception:
pec.delete() # If we get an unexpected exception, be sure to rollback the transaction
# And send it to the new email... transaction.rollback()
user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) raise
return render_to_response("email_change_successful.html", d)
@ensure_csrf_cookie @ensure_csrf_cookie
......
...@@ -8,7 +8,7 @@ and acceptance tests. ...@@ -8,7 +8,7 @@ and acceptance tests.
### Unit Tests ### Unit Tests
* Each test case should be concise: setup, execute, check, and teardown. * Each test case should be concise: setup, execute, check, and teardown.
If you find yourself writing tests with many steps, consider refactoring If you find yourself writing tests with many steps, consider refactoring
the unit under tests into smaller units, and then testing those individually. the unit under tests into smaller units, and then testing those individually.
* As a rule of thumb, your unit tests should cover every code branch. * As a rule of thumb, your unit tests should cover every code branch.
...@@ -16,19 +16,19 @@ the unit under tests into smaller units, and then testing those individually. ...@@ -16,19 +16,19 @@ the unit under tests into smaller units, and then testing those individually.
* Mock or patch external dependencies. * Mock or patch external dependencies.
We use [voidspace mock](http://www.voidspace.org.uk/python/mock/). We use [voidspace mock](http://www.voidspace.org.uk/python/mock/).
* We unit test Python code (using [unittest](http://docs.python.org/2/library/unittest.html)) and * We unit test Python code (using [unittest](http://docs.python.org/2/library/unittest.html)) and
Javascript (using [Jasmine](http://pivotal.github.io/jasmine/)) Javascript (using [Jasmine](http://pivotal.github.io/jasmine/))
### Integration Tests ### Integration Tests
* Test several units at the same time. * Test several units at the same time.
Note that you can still mock or patch dependencies Note that you can still mock or patch dependencies
that are not under test! For example, you might test that that are not under test! For example, you might test that
`LoncapaProblem`, `NumericalResponse`, and `CorrectMap` in the `LoncapaProblem`, `NumericalResponse`, and `CorrectMap` in the
`capa` package work together, while still mocking out template rendering. `capa` package work together, while still mocking out template rendering.
* Use integration tests to ensure that units are hooked up correctly. * Use integration tests to ensure that units are hooked up correctly.
You do not need to test every possible input--that's what unit You do not need to test every possible input--that's what unit
tests are for. Instead, focus on testing the "happy path" tests are for. Instead, focus on testing the "happy path"
to verify that the components work together correctly. to verify that the components work together correctly.
* Many of our tests use the [Django test client](https://docs.djangoproject.com/en/dev/topics/testing/overview/) to simulate * Many of our tests use the [Django test client](https://docs.djangoproject.com/en/dev/topics/testing/overview/) to simulate
...@@ -43,8 +43,8 @@ these tests simulate user interactions through the browser using ...@@ -43,8 +43,8 @@ these tests simulate user interactions through the browser using
Overall, you want to write the tests that **maximize coverage** Overall, you want to write the tests that **maximize coverage**
while **minimizing maintenance**. while **minimizing maintenance**.
In practice, this usually means investing heavily In practice, this usually means investing heavily
in unit tests, which tend to be the most robust to changes in the code base. in unit tests, which tend to be the most robust to changes in the code base.
![Test Pyramid](test_pyramid.png) ![Test Pyramid](test_pyramid.png)
...@@ -53,13 +53,13 @@ and acceptance tests. Most of our tests are unit tests or integration tests. ...@@ -53,13 +53,13 @@ and acceptance tests. Most of our tests are unit tests or integration tests.
## Test Locations ## Test Locations
* Python unit and integration tests: Located in * Python unit and integration tests: Located in
subpackages called `tests`. subpackages called `tests`.
For example, the tests for the `capa` package are located in For example, the tests for the `capa` package are located in
`common/lib/capa/capa/tests`. `common/lib/capa/capa/tests`.
* Javascript unit tests: Located in `spec` folders. For example, * Javascript unit tests: Located in `spec` folders. For example,
`common/lib/xmodule/xmodule/js/spec` and `{cms,lms}/static/coffee/spec` `common/lib/xmodule/xmodule/js/spec` and `{cms,lms}/static/coffee/spec`
For consistency, you should use the same directory structure for implementation For consistency, you should use the same directory structure for implementation
and test. For example, the test for `src/views/module.coffee` and test. For example, the test for `src/views/module.coffee`
should be written in `spec/views/module_spec.coffee`. should be written in `spec/views/module_spec.coffee`.
...@@ -101,7 +101,7 @@ You can run tests using `rake` commands. For example, ...@@ -101,7 +101,7 @@ You can run tests using `rake` commands. For example,
rake test rake test
runs all the tests. It also runs `collectstatic`, which prepares the static files used by the site (for example, compiling Coffeescript to Javascript). runs all the tests. It also runs `collectstatic`, which prepares the static files used by the site (for example, compiling Coffeescript to Javascript).
You can also run the tests without `collectstatic`, which tends to be faster: You can also run the tests without `collectstatic`, which tends to be faster:
...@@ -117,12 +117,11 @@ xmodule can be tested independently, with this: ...@@ -117,12 +117,11 @@ xmodule can be tested independently, with this:
To run a single django test class: To run a single django test class:
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth rake test_lms[courseware.tests.tests:testViewAuth]
To run a single django test: To run a single django test:
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth.test_dark_launch rake test_lms[courseware.tests.tests:TestViewAuth.test_dark_launch]
To run a single nose test file: To run a single nose test file:
...@@ -150,7 +149,7 @@ If the `phantomjs` binary is not on the path, set the `PHANTOMJS_PATH` environme ...@@ -150,7 +149,7 @@ If the `phantomjs` binary is not on the path, set the `PHANTOMJS_PATH` environme
PHANTOMJS_PATH=/path/to/phantomjs rake phantomjs_jasmine_{lms,cms} PHANTOMJS_PATH=/path/to/phantomjs rake phantomjs_jasmine_{lms,cms}
Once you have run the `rake` command, your browser should open to Once you have run the `rake` command, your browser should open to
to `http://localhost/_jasmine/`, which displays the test results. to `http://localhost/_jasmine/`, which displays the test results.
**Troubleshooting**: If you get an error message while running the `rake` task, **Troubleshooting**: If you get an error message while running the `rake` task,
...@@ -163,7 +162,7 @@ Most of our tests use [Splinter](http://splinter.cobrateam.info/) ...@@ -163,7 +162,7 @@ Most of our tests use [Splinter](http://splinter.cobrateam.info/)
to simulate UI browser interactions. Splinter, in turn, to simulate UI browser interactions. Splinter, in turn,
uses [Selenium](http://docs.seleniumhq.org/) to control the Chrome browser. uses [Selenium](http://docs.seleniumhq.org/) to control the Chrome browser.
**Prerequisite**: You must have [ChromeDriver](https://code.google.com/p/selenium/wiki/ChromeDriver) **Prerequisite**: You must have [ChromeDriver](https://code.google.com/p/selenium/wiki/ChromeDriver)
installed to run the tests in Chrome. The tests are confirmed to run installed to run the tests in Chrome. The tests are confirmed to run
with Chrome (not Chromium) version 26.0.0.1410.63 with ChromeDriver with Chrome (not Chromium) version 26.0.0.1410.63 with ChromeDriver
version r195636. version r195636.
...@@ -190,7 +189,7 @@ Try running: ...@@ -190,7 +189,7 @@ Try running:
pip install -r requirements.txt pip install -r requirements.txt
**Note**: The acceptance tests can *not* currently run in parallel. **Note**: The acceptance tests can *not* currently run in parallel.
## Viewing Test Coverage ## Viewing Test Coverage
......
...@@ -73,8 +73,8 @@ rake pylint > pylint.log || cat pylint.log ...@@ -73,8 +73,8 @@ rake pylint > pylint.log || cat pylint.log
TESTS_FAILED=0 TESTS_FAILED=0
# Run the python unit tests # Run the python unit tests
rake test_cms[false] || TESTS_FAILED=1 rake test_cms || TESTS_FAILED=1
rake test_lms[false] || TESTS_FAILED=1 rake test_lms || TESTS_FAILED=1
rake test_common/lib/capa || TESTS_FAILED=1 rake test_common/lib/capa || TESTS_FAILED=1
rake test_common/lib/xmodule || TESTS_FAILED=1 rake test_common/lib/xmodule || TESTS_FAILED=1
......
<h1>E-mail change failed.</h1>
<p>We were unable to send a confirmation email to ${email}</p>
...@@ -110,7 +110,9 @@ generated-members= ...@@ -110,7 +110,9 @@ generated-members=
get_url, get_url,
size, size,
content, content,
status_code status_code,
# For factory_body factories
create
[BASIC] [BASIC]
......
...@@ -12,10 +12,11 @@ def run_under_coverage(cmd, root) ...@@ -12,10 +12,11 @@ def run_under_coverage(cmd, root)
return cmd return cmd
end end
def run_tests(system, report_dir, stop_on_failure=true) def run_tests(system, report_dir, test_id=nil, stop_on_failure=true)
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
dirs = Dir["common/djangoapps/*"] + Dir["#{system}/djangoapps/*"] dirs = Dir["common/djangoapps/*"] + Dir["#{system}/djangoapps/*"]
cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', *dirs.each) test_id = dirs.join(' ') if test_id.nil? or test_id == ''
cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', test_id)
sh(run_under_coverage(cmd, system)) do |ok, res| sh(run_under_coverage(cmd, system)) do |ok, res|
if !ok and stop_on_failure if !ok and stop_on_failure
abort "Test failed!" abort "Test failed!"
...@@ -44,13 +45,13 @@ TEST_TASK_DIRS = [] ...@@ -44,13 +45,13 @@ TEST_TASK_DIRS = []
# Per System tasks # Per System tasks
desc "Run all django tests on our djangoapps for the #{system}" desc "Run all django tests on our djangoapps for the #{system}"
task "test_#{system}", [:stop_on_failure] => ["clean_test_files", :predjango, "#{system}:gather_assets:test", "fasttest_#{system}"] task "test_#{system}", [:test_id, :stop_on_failure] => ["clean_test_files", :predjango, "#{system}:gather_assets:test", "fasttest_#{system}"]
# Have a way to run the tests without running collectstatic -- useful when debugging without # Have a way to run the tests without running collectstatic -- useful when debugging without
# messing with static files. # messing with static files.
task "fasttest_#{system}", [:stop_on_failure] => [report_dir, :install_prereqs, :predjango] do |t, args| task "fasttest_#{system}", [:test_id, :stop_on_failure] => [report_dir, :install_prereqs, :predjango] do |t, args|
args.with_defaults(:stop_on_failure => 'true') args.with_defaults(:stop_on_failure => 'true', :test_id => nil)
run_tests(system, report_dir, args.stop_on_failure) run_tests(system, report_dir, args.test_id, args.stop_on_failure)
end end
# Run acceptance tests # Run acceptance tests
...@@ -100,7 +101,7 @@ end ...@@ -100,7 +101,7 @@ end
task :test do task :test do
TEST_TASK_DIRS.each do |dir| TEST_TASK_DIRS.each do |dir|
Rake::Task["test_#{dir}"].invoke(false) Rake::Task["test_#{dir}"].invoke(nil, false)
end end
if $failed_tests > 0 if $failed_tests > 0
......
...@@ -71,7 +71,7 @@ transifex-client==0.8 ...@@ -71,7 +71,7 @@ transifex-client==0.8
coverage==3.6 coverage==3.6
factory_boy==2.0.2 factory_boy==2.0.2
lettuce==0.2.16 lettuce==0.2.16
mock==0.8.0 mock==1.0.1
nosexcover==1.0.7 nosexcover==1.0.7
pep8==1.4.5 pep8==1.4.5
pylint==0.28 pylint==0.28
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment