diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_force_publish.py b/cms/djangoapps/contentstore/management/commands/tests/test_force_publish.py index 7358210b31..5fd69318e6 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_force_publish.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_force_publish.py @@ -4,7 +4,7 @@ Tests for the force_publish management command import mock from django.core.management import call_command, CommandError from xmodule.modulestore import ModuleStoreEnum -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase, ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from contentstore.management.commands.force_publish import Command from contentstore.management.commands.utils import get_course_versions @@ -62,7 +62,19 @@ class TestForcePublish(SharedModuleStoreTestCase): with self.assertRaisesRegexp(CommandError, errstring): call_command('force_publish', unicode(course.id)) - @SharedModuleStoreTestCase.modifies_courseware + +class TestForcePublishModifications(ModuleStoreTestCase): + """ + Tests for the force_publish management command that modify the courseware + during the test. + """ + + def setUp(self): + super(TestForcePublishModifications, self).setUp() + self.course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split) + self.test_user_id = ModuleStoreEnum.UserID.test + self.command = Command() + def test_force_publish(self): """ Test 'force_publish' command diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 7bb105d191..8965eb8659 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -88,8 +88,9 @@ class ContentStoreTestCase(ModuleStoreTestCase): class AuthTestCase(ContentStoreTestCase): """Check that various permissions-related things work""" - + CREATE_USER = False + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] def setUp(self): super(AuthTestCase, self).setUp() diff --git a/common/djangoapps/embargo/tests/test_api.py b/common/djangoapps/embargo/tests/test_api.py index 217aab0927..fc4a5c77b4 100644 --- a/common/djangoapps/embargo/tests/test_api.py +++ b/common/djangoapps/embargo/tests/test_api.py @@ -46,6 +46,8 @@ MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}) class EmbargoCheckAccessApiTests(ModuleStoreTestCase): """Test the embargo API calls to determine whether a user has access. """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): super(EmbargoCheckAccessApiTests, self).setUp() self.course = CourseFactory.create() @@ -239,16 +241,13 @@ class EmbargoMessageUrlApiTests(UrlResetMixin, ModuleStoreTestCase): """Test the embargo API calls for retrieving the blocking message URLs. """ URLCONF_MODULES = ['embargo'] + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] @patch.dict(settings.FEATURES, {'EMBARGO': True}) def setUp(self): super(EmbargoMessageUrlApiTests, self).setUp() self.course = CourseFactory.create() - def tearDown(self): - super(EmbargoMessageUrlApiTests, self).tearDown() - cache.clear() - @ddt.data( ('enrollment', '/embargo/blocked-message/enrollment/embargo/'), ('courseware', '/embargo/blocked-message/courseware/embargo/') diff --git a/common/djangoapps/embargo/tests/test_models.py b/common/djangoapps/embargo/tests/test_models.py index 340aabe829..5fc93bb868 100644 --- a/common/djangoapps/embargo/tests/test_models.py +++ b/common/djangoapps/embargo/tests/test_models.py @@ -8,9 +8,14 @@ from embargo.models import ( Country, CountryAccessRule, CourseAccessRuleHistory ) +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase -class EmbargoModelsTest(TestCase): + +class EmbargoModelsTest(CacheIsolationTestCase): """Test each of the 3 models in embargo.models""" + + ENABLED_CACHES = ['default'] + def test_course_embargo(self): course_id = CourseLocator('abc', '123', 'doremi') # Test that course is not authorized by default @@ -101,9 +106,11 @@ class EmbargoModelsTest(TestCase): self.assertFalse('1.2.0.0' in cblacklist) -class RestrictedCourseTest(TestCase): +class RestrictedCourseTest(CacheIsolationTestCase): """Test RestrictedCourse model. """ + ENABLED_CACHES = ['default'] + def test_unicode_values(self): course_id = CourseLocator('abc', '123', 'doremi') restricted_course = RestrictedCourse.objects.create(course_key=course_id) @@ -162,8 +169,9 @@ class CountryTest(TestCase): self.assertEquals(unicode(country), "New Zealand (NZ)") -class CountryAccessRuleTest(TestCase): +class CountryAccessRuleTest(CacheIsolationTestCase): """Test CountryAccessRule model. """ + ENABLED_CACHES = ['default'] def test_unicode_values(self): course_id = CourseLocator('abc', '123', 'doremi') diff --git a/common/djangoapps/embargo/tests/test_views.py b/common/djangoapps/embargo/tests/test_views.py index 09b3a48781..6ef81f6618 100644 --- a/common/djangoapps/embargo/tests/test_views.py +++ b/common/djangoapps/embargo/tests/test_views.py @@ -10,11 +10,12 @@ import ddt from util.testing import UrlResetMixin from embargo import messages +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @ddt.ddt -class CourseAccessMessageViewTest(UrlResetMixin, TestCase): +class CourseAccessMessageViewTest(CacheIsolationTestCase, UrlResetMixin): """Tests for the courseware access message view. These end-points serve static content. @@ -32,6 +33,8 @@ class CourseAccessMessageViewTest(UrlResetMixin, TestCase): """ + ENABLED_CACHES = ['default'] + URLCONF_MODULES = ['embargo'] @patch.dict(settings.FEATURES, {'EMBARGO': True}) diff --git a/common/djangoapps/enrollment/tests/test_api.py b/common/djangoapps/enrollment/tests/test_api.py index 95b441d157..f941db37e9 100644 --- a/common/djangoapps/enrollment/tests/test_api.py +++ b/common/djangoapps/enrollment/tests/test_api.py @@ -15,22 +15,24 @@ from course_modes.models import CourseMode from enrollment import api from enrollment.errors import EnrollmentApiLoadError, EnrollmentNotFoundError, CourseModeNotFoundError from enrollment.tests import fake_data_api +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase @ddt.ddt @override_settings(ENROLLMENT_DATA_API="enrollment.tests.fake_data_api") @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class EnrollmentTest(TestCase): +class EnrollmentTest(CacheIsolationTestCase): """ Test student enrollment, especially with different course modes. """ USERNAME = "Bob" COURSE_ID = "some/great/course" + ENABLED_CACHES = ['default'] + def setUp(self): super(EnrollmentTest, self).setUp() fake_data_api.reset() - cache.clear() @ddt.data( # Default (no course modes in the database) diff --git a/common/djangoapps/enrollment/tests/test_views.py b/common/djangoapps/enrollment/tests/test_views.py index 722709bc29..d20c31a0aa 100644 --- a/common/djangoapps/enrollment/tests/test_views.py +++ b/common/djangoapps/enrollment/tests/test_views.py @@ -141,6 +141,8 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase): OTHER_USERNAME = "Jane" OTHER_EMAIL = "jane@example.com" + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): """ Create a course and user, then log in. """ super(EnrollmentTest, self).setUp() diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py index ee1e066ba0..6d58bd16a8 100644 --- a/common/djangoapps/external_auth/tests/test_shib.py +++ b/common/djangoapps/external_auth/tests/test_shib.py @@ -23,11 +23,12 @@ from mock import patch from nose.plugins.attrib import attr from urllib import urlencode +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.views import create_account, change_enrollment from student.models import UserProfile, CourseEnrollment from student.tests.factories import UserFactory from xmodule.modulestore.tests.factories import CourseFactory -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore import ModuleStoreEnum @@ -76,11 +77,14 @@ def gen_all_identities(): @attr('shard_3') @ddt @override_settings(SESSION_ENGINE='django.contrib.sessions.backends.cache') -class ShibSPTest(SharedModuleStoreTestCase): +class ShibSPTest(CacheIsolationTestCase): """ Tests for the Shibboleth SP, which communicates via request.META (Apache environment variables set by mod_shib) """ + + ENABLED_CACHES = ['default'] + request_factory = RequestFactory() def setUp(self): @@ -377,8 +381,23 @@ class ShibSPTest(SharedModuleStoreTestCase): self.assertEqual(profile.name, request2.session['ExternalAuthMap'].external_name) self.assertEqual(profile.name, identity.get('displayName').decode('utf-8')) + +@ddt +@override_settings(SESSION_ENGINE='django.contrib.sessions.backends.cache') +class ShibSPTestModifiedCourseware(ModuleStoreTestCase): + """ + Tests for the Shibboleth SP which modify the courseware + """ + + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + + request_factory = RequestFactory() + + def setUp(self): + super(ShibSPTestModifiedCourseware, self).setUp() + self.test_user_id = ModuleStoreEnum.UserID.test + @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") - @SharedModuleStoreTestCase.modifies_courseware @data(None, "", "shib:https://idp.stanford.edu/") def test_course_specific_login_and_reg(self, domain): """ @@ -457,7 +476,6 @@ class ShibSPTest(SharedModuleStoreTestCase): '&enrollment_action=enroll') @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") - @SharedModuleStoreTestCase.modifies_courseware def test_enrollment_limit_by_domain(self): """ Tests that the enrollmentDomain setting is properly limiting enrollment to those who have @@ -525,7 +543,6 @@ class ShibSPTest(SharedModuleStoreTestCase): self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") - @SharedModuleStoreTestCase.modifies_courseware def test_shib_login_enrollment(self): """ A functionality test that a student with an existing shib login diff --git a/common/djangoapps/student/tests/test_login.py b/common/djangoapps/student/tests/test_login.py index 1a80873a78..2c0bce87cf 100644 --- a/common/djangoapps/student/tests/test_login.py +++ b/common/djangoapps/student/tests/test_login.py @@ -17,6 +17,7 @@ from mock import patch from social.apps.django_app.default.models import UserSocialAuth from external_auth.models import ExternalAuthMap +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.tests.factories import UserFactory, RegistrationFactory, UserProfileFactory from student.views import login_oauth_token from third_party_auth.tests.utils import ( @@ -28,11 +29,13 @@ from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -class LoginTest(TestCase): +class LoginTest(CacheIsolationTestCase): ''' Test student.views.login_user() view ''' + ENABLED_CACHES = ['default'] + def setUp(self): super(LoginTest, self).setUp() # Create one user and save it to the database diff --git a/common/djangoapps/student/tests/test_login_registration_forms.py b/common/djangoapps/student/tests/test_login_registration_forms.py index d7f8713069..ada18b7db7 100644 --- a/common/djangoapps/student/tests/test_login_registration_forms.py +++ b/common/djangoapps/student/tests/test_login_registration_forms.py @@ -158,7 +158,7 @@ class RegisterFormTest(ThirdPartyAuthTestMixin, UrlResetMixin, SharedModuleStore """Test rendering of the registration form. """ URLCONF_MODULES = ['lms.urls'] - + @classmethod def setUpClass(cls): super(RegisterFormTest, cls).setUpClass() diff --git a/common/djangoapps/student/tests/test_reset_password.py b/common/djangoapps/student/tests/test_reset_password.py index ac07a0c4e0..63e77e780f 100644 --- a/common/djangoapps/student/tests/test_reset_password.py +++ b/common/djangoapps/student/tests/test_reset_password.py @@ -19,6 +19,7 @@ from django.utils.http import urlsafe_base64_encode, base36_to_int, int_to_base3 from mock import Mock, patch import ddt +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.views import password_reset, password_reset_confirm_wrapper, SETTING_CHANGE_INITIATED from student.tests.factories import UserFactory from student.tests.test_email import mock_render_to_string @@ -28,11 +29,13 @@ from .test_microsite import fake_microsite_get_value @ddt.ddt -class ResetPasswordTests(EventTestMixin, TestCase): +class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): """ Tests that clicking reset password sends email, and doesn't activate the user """ request_factory = RequestFactory() + ENABLED_CACHES = ['default'] + def setUp(self): super(ResetPasswordTests, self).setUp('student.views.tracker') self.user = UserFactory.create() diff --git a/common/djangoapps/student/tests/test_user_profile_properties.py b/common/djangoapps/student/tests/test_user_profile_properties.py index 3c7b835d54..ace966e726 100644 --- a/common/djangoapps/student/tests/test_user_profile_properties.py +++ b/common/djangoapps/student/tests/test_user_profile_properties.py @@ -7,14 +7,17 @@ from django.test import TestCase from student.models import UserProfile from student.tests.factories import UserFactory from django.core.cache import cache +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase @ddt.ddt -class UserProfilePropertiesTest(TestCase): +class UserProfilePropertiesTest(CacheIsolationTestCase): """Unit tests for age, gender_display, and level_of_education_display properties .""" password = "test" + ENABLED_CACHES = ['default'] + def setUp(self): super(UserProfilePropertiesTest, self).setUp() self.user = UserFactory.create(password=self.password) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 0b3108bdb9..6bebed9acc 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -13,7 +13,6 @@ from django.conf import settings from django.contrib.auth.models import User from django.test import TestCase from django.test.utils import override_settings -from request_cache.middleware import RequestCache from courseware.field_overrides import OverrideFieldData # pylint: disable=import-error from openedx.core.lib.tempdir import mkdtemp_clean @@ -25,6 +24,7 @@ from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOS from xmodule.modulestore.tests.factories import XMODULE_FACTORY_LOCK from openedx.core.djangoapps.bookmarks.signals import trigger_update_xblocks_cache_task +from openedx.core.djangolib.testing.utils import CacheIsolationMixin, CacheIsolationTestCase class StoreConstructors(object): @@ -170,23 +170,64 @@ TEST_DATA_SPLIT_MODULESTORE = mixed_store_config( ) -def clear_all_caches(): - """Clear all caches so that cache info doesn't leak across test cases.""" - # This will no longer be necessary when Django adds (in Django 1.10?): - # https://code.djangoproject.com/ticket/11505 - for cache in django.core.cache.caches.all(): - cache.clear() +class ModuleStoreIsolationMixin(CacheIsolationMixin): + """ + A mixin to be used by TestCases that want to isolate their use of the + Modulestore. - RequestCache().clear_request_cache() + How to use:: + + class MyTestCase(ModuleStoreMixin, TestCase): + + MODULESTORE = + + def my_test(self): + self.start_modulestore_isolation() + self.addCleanup(self.end_modulestore_isolation) + + modulestore.create_course(...) + ... + + """ + + MODULESTORE = mixed_store_config(mkdtemp_clean(), {}) + ENABLED_CACHES = ['mongo_metadata_inheritance', 'loc_cache'] + + @classmethod + def start_modulestore_isolation(cls): + """ + Isolate uses of the modulestore after this call. Once + :py:meth:`end_modulestore_isolation` is called, this modulestore will + be flushed (all content will be deleted). + """ + cls.start_cache_isolation() + cls.__settings_override = override_settings( + MODULESTORE=cls.MODULESTORE, + ) + cls.__settings_override.__enter__() + XMODULE_FACTORY_LOCK.enable() + clear_existing_modulestores() + cls.store = modulestore() + + @classmethod + def end_modulestore_isolation(cls): + """ + Delete all content in the Modulestore, and reset the Modulestore + settings from before :py:meth:`start_modulestore_isolation` was + called. + """ + drop_mongo_collections() # pylint: disable=no-value-for-parameter + XMODULE_FACTORY_LOCK.disable() + cls.__settings_override.__exit__(None, None, None) + cls.end_cache_isolation() -class SharedModuleStoreTestCase(TestCase): +class SharedModuleStoreTestCase(ModuleStoreIsolationMixin, CacheIsolationTestCase): """ Subclass for any test case that uses a ModuleStore that can be shared between individual tests. This class ensures that the ModuleStore is cleaned before/after the entire test case has run. Use this class if your tests - set up one or a small number of courses that individual tests do not modify - (or modify extermely rarely -- see @modifies_courseware). + set up one or a small number of courses that individual tests do not modify. If your tests modify contents in the ModuleStore, you should use ModuleStoreTestCase instead. @@ -218,41 +259,26 @@ class SharedModuleStoreTestCase(TestCase): In Django 1.8, we will be able to use setUpTestData() to do class level init for Django ORM models that will get cleaned up properly. """ - MODULESTORE = mixed_store_config(mkdtemp_clean(), {}) # Tell Django to clean out all databases, not just default multi_db = True - @classmethod - def _setUpModuleStore(cls): # pylint: disable=invalid-name - """ - Set up the modulestore for an entire test class. - """ - cls._settings_override = override_settings(MODULESTORE=cls.MODULESTORE) - cls._settings_override.__enter__() - XMODULE_FACTORY_LOCK.enable() - clear_existing_modulestores() - cls.store = modulestore() - @classmethod @contextmanager def setUpClassAndTestData(cls): # pylint: disable=invalid-name """ For use when the test class has a setUpTestData() method that uses variables that are setup during setUpClass() of the same test class. - Use it like so: - @classmethod def setUpClass(cls): with super(MyTestClass, cls).setUpClassAndTestData(): - @classmethod def setUpTestData(cls): """ - cls._setUpModuleStore() + cls.start_modulestore_isolation() # Now yield to allow the test class to run its setUpClass() setup code. yield # Now call the base class, which calls back into the test class's setUpTestData(). @@ -261,19 +287,15 @@ class SharedModuleStoreTestCase(TestCase): @classmethod def setUpClass(cls): """ - For use when the test class has no setUpTestData() method -or- - when that method does not use variable set up in setUpClass(). + Start modulestore isolation, and then load modulestore specific + test data. """ super(SharedModuleStoreTestCase, cls).setUpClass() - cls._setUpModuleStore() + cls.start_modulestore_isolation() @classmethod def tearDownClass(cls): - drop_mongo_collections() # pylint: disable=no-value-for-parameter - clear_all_caches() - XMODULE_FACTORY_LOCK.disable() - cls._settings_override.__exit__(None, None, None) - + cls.end_modulestore_isolation() super(SharedModuleStoreTestCase, cls).tearDownClass() def setUp(self): @@ -282,69 +304,8 @@ class SharedModuleStoreTestCase(TestCase): OverrideFieldData.provider_classes = None super(SharedModuleStoreTestCase, self).setUp() - def tearDown(self): - """Reset caches.""" - clear_all_caches() - super(SharedModuleStoreTestCase, self).tearDown() - def reset(self): - """ - Manually run tearDownClass/setUpClass again. - - This is so that if you have a mostly read-only course that you're just - modifying in one test, you can write `self.reset()` at the - end of that test and reset the state of the world for other tests in - the class. - """ - self.tearDownClass() - self.setUpClass() - - @staticmethod - def modifies_courseware(f): - """ - Decorator to place around tests that modify course content. - - For performance reasons, SharedModuleStoreTestCase intentionally does - not reset the modulestore between individual tests. However, sometimes - you might have a test case where the vast majority of tests treat a - course as read-only, but one or two want to modify it. In that case, you - can do this: - - class MyTestCase(SharedModuleStoreTestCase): - # ... - @SharedModuleStoreTestCase.modifies_courseware - def test_that_edits_modulestore(self): - do_something() - - This is equivalent to calling `self.reset()` at the end of - your test. - - If you find yourself using this functionality a lot, it might indicate - that you should be using ModuleStoreTestCase instead, or that you should - break up your tests into different TestCases. - """ - @functools.wraps(f) - def wrapper(*args, **kwargs): - """Call the object method, and reset the test case afterwards.""" - try: - # Attempt execution of the test. - return_val = f(*args, **kwargs) - except: - # If the test raises an exception, re-raise it. - raise - else: - # Otherwise, return the test's return value. - return return_val - finally: - # In either case, call SharedModuleStoreTestCase.reset() "on the way out." - # For more, see here: https://docs.python.org/2/tutorial/errors.html#defining-clean-up-actions. - obj = args[0] - obj.reset() - - return wrapper - - -class ModuleStoreTestCase(TestCase): +class ModuleStoreTestCase(ModuleStoreIsolationMixin, TestCase): """ Subclass for any test case that uses a ModuleStore. Ensures that the ModuleStore is cleaned before/after each test. @@ -382,8 +343,6 @@ class ModuleStoreTestCase(TestCase): your `setUp()` method. """ - MODULESTORE = mixed_store_config(mkdtemp_clean(), {}) - CREATE_USER = True # Tell Django to clean out all databases, not just default @@ -394,20 +353,9 @@ class ModuleStoreTestCase(TestCase): Creates a test User if `self.CREATE_USER` is True. Sets the password as self.user_password. """ - settings_override = override_settings(MODULESTORE=self.MODULESTORE) - settings_override.__enter__() - self.addCleanup(settings_override.__exit__, None, None, None) + self.start_modulestore_isolation() - # Clear out any existing modulestores, - # which will cause them to be re-created - clear_existing_modulestores() - - self.addCleanup(drop_mongo_collections) - self.addCleanup(clear_all_caches) - - # Enable XModuleFactories for the space of this test (and its setUp). - self.addCleanup(XMODULE_FACTORY_LOCK.disable) - XMODULE_FACTORY_LOCK.enable() + self.addCleanup(self.end_modulestore_isolation) # When testing CCX, we should make sure that # OverrideFieldData.provider_classes is always reset to `None` so @@ -436,7 +384,6 @@ class ModuleStoreTestCase(TestCase): self.user.is_staff = True self.user.save() - def create_non_staff_user(self): """ Creates a non-staff test user. diff --git a/lms/djangoapps/bulk_email/tests/test_err_handling.py b/lms/djangoapps/bulk_email/tests/test_err_handling.py index 04547d7e9c..d6788e6817 100644 --- a/lms/djangoapps/bulk_email/tests/test_err_handling.py +++ b/lms/djangoapps/bulk_email/tests/test_err_handling.py @@ -44,6 +44,8 @@ class TestEmailErrors(ModuleStoreTestCase): Test that errors from sending email are handled properly. """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): super(TestEmailErrors, self).setUp() course_title = u"ẗëṡẗ title イ乇丂イ ᄊ乇丂丂ムg乇 キo尺 ムレレ тэѕт мэѕѕаБэ" diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index 92f32e773f..82f8f3467f 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -52,6 +52,8 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, # TEST_DATA must be overridden by subclasses TEST_DATA = None + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): """ Create a test client, course, and user. diff --git a/lms/djangoapps/ccx/tests/test_views.py b/lms/djangoapps/ccx/tests/test_views.py index 507a8d66f2..e7839c1a1b 100644 --- a/lms/djangoapps/ccx/tests/test_views.py +++ b/lms/djangoapps/ccx/tests/test_views.py @@ -68,6 +68,8 @@ from lms.djangoapps.ccx.tests.utils import ( from lms.djangoapps.ccx.utils import is_email from lms.djangoapps.ccx.views import get_date +from xmodule.modulestore.django import modulestore + def intercept_renderer(path, context): """ @@ -184,10 +186,6 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): Tests for Custom Courses views. """ - @classmethod - def setUpClass(cls): - super(TestCoachDashboard, cls).setUpClass() - def setUp(self): """ Set up tests @@ -206,31 +204,6 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): allow_access(self.course, instructor, 'instructor') self.assertTrue(CourseInstructorRole(self.course.id).has_user(instructor)) - def assert_elements_in_schedule(self, url, n_chapters=2, n_sequentials=4, n_verticals=8): - """ - Helper function to count visible elements in the schedule - """ - response = self.client.get(url) - # the schedule contains chapters - chapters = json.loads(response.mako_context['schedule']) # pylint: disable=no-member - sequentials = flatten([chapter.get('children', []) for chapter in chapters]) - verticals = flatten([sequential.get('children', []) for sequential in sequentials]) - # check that the numbers of nodes at different level are the expected ones - self.assertEqual(n_chapters, len(chapters)) - self.assertEqual(n_sequentials, len(sequentials)) - self.assertEqual(n_verticals, len(verticals)) - # extract the locations of all the nodes - all_elements = chapters + sequentials + verticals - return [elem['location'] for elem in all_elements if 'location' in elem] - - def hide_node(self, node): - """ - Helper function to set the node `visible_to_staff_only` property - to True and save the change - """ - node.visible_to_staff_only = True - self.mstore.update_item(node, self.coach.id) - def test_not_a_coach(self): """ User is not a coach, should get Forbidden response. @@ -352,43 +325,6 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): self.assertEqual(get_date(ccx, unit, 'start', parent_node=subsection), self.mooc_start) self.assertEqual(get_date(ccx, unit, 'due', parent_node=subsection), self.mooc_due) - @SharedModuleStoreTestCase.modifies_courseware - @patch('ccx.views.render_to_response', intercept_renderer) - @patch('ccx.views.TODAY') - def test_get_ccx_schedule(self, today): - """ - Gets CCX schedule and checks number of blocks in it. - Hides nodes at a different depth and checks that these nodes - are not in the schedule. - """ - today.return_value = datetime.datetime(2014, 11, 25, tzinfo=pytz.UTC) - self.make_coach() - ccx = self.make_ccx() - url = reverse( - 'ccx_coach_dashboard', - kwargs={ - 'course_id': CCXLocator.from_course_locator( - self.course.id, ccx.id) - } - ) - # all the elements are visible - self.assert_elements_in_schedule(url) - # hide a vertical - vertical = self.verticals[0] - self.hide_node(vertical) - locations = self.assert_elements_in_schedule(url, n_verticals=7) - self.assertNotIn(unicode(vertical.location), locations) - # hide a sequential - sequential = self.sequentials[0] - self.hide_node(sequential) - locations = self.assert_elements_in_schedule(url, n_sequentials=3, n_verticals=6) - self.assertNotIn(unicode(sequential.location), locations) - # hide a chapter - chapter = self.chapters[0] - self.hide_node(chapter) - locations = self.assert_elements_in_schedule(url, n_chapters=1, n_sequentials=2, n_verticals=4) - self.assertNotIn(unicode(chapter.location), locations) - @patch('ccx.views.render_to_response', intercept_renderer) @patch('ccx.views.TODAY') def test_edit_schedule(self, today): @@ -842,6 +778,134 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): ) +@attr('shard_1') +class TestCoachDashboardSchedule(CcxTestCase, LoginEnrollmentTestCase, ModuleStoreTestCase): + """ + Tests of the CCX Coach Dashboard which need to modify the course content. + """ + + ENABLED_CACHES = ['default', 'mongo_inheritance_cache', 'loc_cache'] + + def setUp(self): + super(TestCoachDashboardSchedule, self).setUp() + self.course = course = CourseFactory.create() + + # Create a course outline + self.mooc_start = start = datetime.datetime( + 2010, 5, 12, 2, 42, tzinfo=pytz.UTC + ) + self.mooc_due = due = datetime.datetime( + 2010, 7, 7, 0, 0, tzinfo=pytz.UTC + ) + + self.chapters = [ + ItemFactory.create(start=start, parent=course) for _ in xrange(2) + ] + self.sequentials = flatten([ + [ + ItemFactory.create(parent=chapter) for _ in xrange(2) + ] for chapter in self.chapters + ]) + self.verticals = flatten([ + [ + ItemFactory.create( + start=start, due=due, parent=sequential, graded=True, format='Homework', category=u'vertical' + ) for _ in xrange(2) + ] for sequential in self.sequentials + ]) + + # Trying to wrap the whole thing in a bulk operation fails because it + # doesn't find the parents. But we can at least wrap this part... + with self.store.bulk_operations(course.id, emit_signals=False): + blocks = flatten([ # pylint: disable=unused-variable + [ + ItemFactory.create(parent=vertical) for _ in xrange(2) + ] for vertical in self.verticals + ]) + + # Create instructor account + self.coach = UserFactory.create() + # create an instance of modulestore + self.mstore = modulestore() + + # Login with the instructor account + self.client.login(username=self.coach.username, password="test") + + # adding staff to master course. + staff = UserFactory() + allow_access(self.course, staff, 'staff') + self.assertTrue(CourseStaffRole(self.course.id).has_user(staff)) + + # adding instructor to master course. + instructor = UserFactory() + allow_access(self.course, instructor, 'instructor') + self.assertTrue(CourseInstructorRole(self.course.id).has_user(instructor)) + + self.assertTrue(modulestore().has_course(self.course.id)) + + def assert_elements_in_schedule(self, url, n_chapters=2, n_sequentials=4, n_verticals=8): + """ + Helper function to count visible elements in the schedule + """ + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + # the schedule contains chapters + chapters = json.loads(response.mako_context['schedule']) # pylint: disable=no-member + sequentials = flatten([chapter.get('children', []) for chapter in chapters]) + verticals = flatten([sequential.get('children', []) for sequential in sequentials]) + # check that the numbers of nodes at different level are the expected ones + self.assertEqual(n_chapters, len(chapters)) + self.assertEqual(n_sequentials, len(sequentials)) + self.assertEqual(n_verticals, len(verticals)) + # extract the locations of all the nodes + all_elements = chapters + sequentials + verticals + return [elem['location'] for elem in all_elements if 'location' in elem] + + def hide_node(self, node): + """ + Helper function to set the node `visible_to_staff_only` property + to True and save the change + """ + node.visible_to_staff_only = True + self.mstore.update_item(node, self.coach.id) + + @patch('ccx.views.render_to_response', intercept_renderer) + @patch('ccx.views.TODAY') + def test_get_ccx_schedule(self, today): + """ + Gets CCX schedule and checks number of blocks in it. + Hides nodes at a different depth and checks that these nodes + are not in the schedule. + """ + today.return_value = datetime.datetime(2014, 11, 25, tzinfo=pytz.UTC) + self.make_coach() + ccx = self.make_ccx() + url = reverse( + 'ccx_coach_dashboard', + kwargs={ + 'course_id': CCXLocator.from_course_locator( + self.course.id, ccx.id) + } + ) + # all the elements are visible + self.assert_elements_in_schedule(url) + # hide a vertical + vertical = self.verticals[0] + self.hide_node(vertical) + locations = self.assert_elements_in_schedule(url, n_verticals=7) + self.assertNotIn(unicode(vertical.location), locations) + # hide a sequential + sequential = self.sequentials[0] + self.hide_node(sequential) + locations = self.assert_elements_in_schedule(url, n_sequentials=3, n_verticals=6) + self.assertNotIn(unicode(sequential.location), locations) + # hide a chapter + chapter = self.chapters[0] + self.hide_node(chapter) + locations = self.assert_elements_in_schedule(url, n_chapters=1, n_sequentials=2, n_verticals=4) + self.assertNotIn(unicode(chapter.location), locations) + + GET_CHILDREN = XModuleMixin.get_children diff --git a/lms/djangoapps/certificates/tests/test_views.py b/lms/djangoapps/certificates/tests/test_views.py index 6824a1ff07..85dc913d31 100644 --- a/lms/djangoapps/certificates/tests/test_views.py +++ b/lms/djangoapps/certificates/tests/test_views.py @@ -12,6 +12,7 @@ from django.test.client import Client from django.test.utils import override_settings from nose.plugins.attrib import attr from opaque_keys.edx.locator import CourseLocator +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from certificates.api import get_certificate_url from certificates.models import ( @@ -38,7 +39,7 @@ FEATURES_WITH_CUSTOM_CERTS_ENABLED.update(FEATURES_WITH_CERTS_ENABLED) @attr('shard_1') @ddt.ddt -class UpdateExampleCertificateViewTest(TestCase): +class UpdateExampleCertificateViewTest(CacheIsolationTestCase): """Tests for the XQueue callback that updates example certificates. """ COURSE_KEY = CourseLocator(org='test', course='test', run='test') @@ -48,6 +49,8 @@ class UpdateExampleCertificateViewTest(TestCase): DOWNLOAD_URL = 'http://www.example.com' ERROR_REASON = 'Kaboom!' + ENABLED_CACHES = ['default'] + def setUp(self): super(UpdateExampleCertificateViewTest, self).setUp() self.cert_set = ExampleCertificateSet.objects.create(course_key=self.COURSE_KEY) diff --git a/lms/djangoapps/course_api/tests/test_api.py b/lms/djangoapps/course_api/tests/test_api.py index 5c447ece2c..d8e375f291 100644 --- a/lms/djangoapps/course_api/tests/test_api.py +++ b/lms/djangoapps/course_api/tests/test_api.py @@ -144,13 +144,24 @@ class TestGetCourseList(CourseListTestMixin, SharedModuleStoreTestCase): with self.assertRaises(PermissionDenied): self._make_api_call(anonuser, self.staff_user) - @SharedModuleStoreTestCase.modifies_courseware + +class TestGetCourseListMultipleCourses(CourseListTestMixin, ModuleStoreTestCase): + """ + Test the behavior of the `list_courses` api function (with tests that + modify the courseware). + """ + + def setUp(self): + super(TestGetCourseListMultipleCourses, self).setUp() + self.course = self.create_course() + self.staff_user = self.create_user("staff", is_staff=True) + self.honor_user = self.create_user("honor", is_staff=False) + def test_multiple_courses(self): self.create_course(course='second') courses = self._make_api_call(self.honor_user, self.honor_user) self.assertEqual(len(courses), 2) - @SharedModuleStoreTestCase.modifies_courseware def test_filter_by_org(self): """Verify that courses are filtered by the provided org key.""" # Create a second course to be filtered out of queries. @@ -173,7 +184,6 @@ class TestGetCourseList(CourseListTestMixin, SharedModuleStoreTestCase): all(course.org == self.course.org for course in filtered_courses) ) - @SharedModuleStoreTestCase.modifies_courseware def test_filter(self): # Create a second course to be filtered out of queries. alternate_course = self.create_course(course='mobile', mobile_available=True) diff --git a/lms/djangoapps/course_api/tests/test_views.py b/lms/djangoapps/course_api/tests/test_views.py index 6bf79bbec0..104e8f1713 100644 --- a/lms/djangoapps/course_api/tests/test_views.py +++ b/lms/djangoapps/course_api/tests/test_views.py @@ -7,7 +7,7 @@ from django.core.urlresolvers import reverse from django.test import RequestFactory from nose.plugins.attrib import attr -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase, ModuleStoreTestCase from .mixins import CourseApiFactoryMixin, TEST_PASSWORD from ..views import CourseDetailView @@ -91,7 +91,24 @@ class CourseListViewTestCase(CourseApiTestViewMixin, SharedModuleStoreTestCase): response_to_missing_username = self.verify_response(expected_status_code=200) self.assertIsNotNone(response_to_missing_username.data) # pylint: disable=no-member - @SharedModuleStoreTestCase.modifies_courseware + def test_not_logged_in(self): + self.client.logout() + self.verify_response() + + +class CourseListViewTestCaseMultipleCourses(CourseApiTestViewMixin, ModuleStoreTestCase): + """ + Test responses returned from CourseListView (with tests that modify the + courseware). + """ + + def setUp(self): + super(CourseListViewTestCaseMultipleCourses, self).setUp() + self.course = self.create_course() + self.url = reverse('course-list') + self.staff_user = self.create_user(username='staff', is_staff=True) + self.honor_user = self.create_user(username='honor', is_staff=False) + def test_filter_by_org(self): """Verify that CourseOverviews are filtered by the provided org key.""" self.setup_user(self.staff_user) @@ -116,7 +133,6 @@ class CourseListViewTestCase(CourseApiTestViewMixin, SharedModuleStoreTestCase): all(course['org'] == self.course.org for course in filtered_response.data['results']) # pylint: disable=no-member ) - @SharedModuleStoreTestCase.modifies_courseware def test_filter(self): self.setup_user(self.staff_user) @@ -139,10 +155,6 @@ class CourseListViewTestCase(CourseApiTestViewMixin, SharedModuleStoreTestCase): "testing course_api.views.CourseListView with filter_={}".format(filter_), ) - def test_not_logged_in(self): - self.client.logout() - self.verify_response() - class CourseDetailViewTestCase(CourseApiTestViewMixin, SharedModuleStoreTestCase): """ diff --git a/lms/djangoapps/course_blocks/management/commands/tests/test_generate_course_blocks.py b/lms/djangoapps/course_blocks/management/commands/tests/test_generate_course_blocks.py index 9c384be15f..c0459a5e21 100644 --- a/lms/djangoapps/course_blocks/management/commands/tests/test_generate_course_blocks.py +++ b/lms/djangoapps/course_blocks/management/commands/tests/test_generate_course_blocks.py @@ -15,6 +15,8 @@ class TestGenerateCourseBlocks(ModuleStoreTestCase): """ Tests generate course blocks management command. """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): """ Create courses in modulestore. diff --git a/lms/djangoapps/course_blocks/tests/test_signals.py b/lms/djangoapps/course_blocks/tests/test_signals.py index 9f63279b31..9887087024 100644 --- a/lms/djangoapps/course_blocks/tests/test_signals.py +++ b/lms/djangoapps/course_blocks/tests/test_signals.py @@ -15,6 +15,7 @@ class CourseBlocksSignalTest(EnableTransformerRegistryMixin, ModuleStoreTestCase """ Tests for the Course Blocks signal """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] def setUp(self): super(CourseBlocksSignalTest, self).setUp() diff --git a/lms/djangoapps/course_blocks/transformers/tests/helpers.py b/lms/djangoapps/course_blocks/transformers/tests/helpers.py index af8b5afa77..bdffb4f3ea 100644 --- a/lms/djangoapps/course_blocks/transformers/tests/helpers.py +++ b/lms/djangoapps/course_blocks/transformers/tests/helpers.py @@ -36,6 +36,8 @@ class CourseStructureTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase) """ Helper for test cases that need to build course structures. """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): """ Create users. diff --git a/lms/djangoapps/courseware/tests/test_course_info.py b/lms/djangoapps/courseware/tests/test_course_info.py index 069547e506..76281d5b97 100644 --- a/lms/djangoapps/courseware/tests/test_course_info.py +++ b/lms/djangoapps/courseware/tests/test_course_info.py @@ -36,6 +36,7 @@ class CourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase): """ Tests for the Course Info page """ + @classmethod def setUpClass(cls): super(CourseInfoTestCase, cls).setUpClass() @@ -263,6 +264,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest """ Tests for the info page of self-paced courses. """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] @classmethod def setUpClass(cls): diff --git a/lms/djangoapps/courseware/tests/test_grades.py b/lms/djangoapps/courseware/tests/test_grades.py index c62657347e..f68c7400a6 100644 --- a/lms/djangoapps/courseware/tests/test_grades.py +++ b/lms/djangoapps/courseware/tests/test_grades.py @@ -151,6 +151,8 @@ class TestMaxScoresCache(SharedModuleStoreTestCase): Tests for the MaxScoresCache """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + @classmethod def setUpClass(cls): super(TestMaxScoresCache, cls).setUpClass() diff --git a/lms/djangoapps/courseware/tests/test_submitting_problems.py b/lms/djangoapps/courseware/tests/test_submitting_problems.py index 0892ef6e89..c163c66dbe 100644 --- a/lms/djangoapps/courseware/tests/test_submitting_problems.py +++ b/lms/djangoapps/courseware/tests/test_submitting_problems.py @@ -127,6 +127,8 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl COURSE_SLUG = "100" COURSE_NAME = "test_course" + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): super(TestSubmittingProblems, self).setUp() diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 016a98c5e6..7cde020765 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -970,6 +970,8 @@ class ProgressPageTests(ModuleStoreTestCase): Tests that verify that the progress page works correctly. """ + ENABLED_CACHES = ['default', 'mongo_modulestore_inheritance', 'loc_cache'] + def setUp(self): super(ProgressPageTests, self).setUp() self.request_factory = RequestFactory() diff --git a/lms/djangoapps/dashboard/management/commands/tests/test_git_add_course.py b/lms/djangoapps/dashboard/management/commands/tests/test_git_add_course.py index 555743f0e9..f5550cdfbf 100644 --- a/lms/djangoapps/dashboard/management/commands/tests/test_git_add_course.py +++ b/lms/djangoapps/dashboard/management/commands/tests/test_git_add_course.py @@ -51,6 +51,8 @@ class TestGitAddCourse(SharedModuleStoreTestCase): TEST_BRANCH_COURSE = SlashSeparatedCourseKey('MITx', 'edx4edx_branch', 'edx4edx') GIT_REPO_DIR = settings.GIT_REPO_DIR + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def assertCommandFailureRegexp(self, regex, *args): """ Convenience function for testing command failures diff --git a/lms/djangoapps/django_comment_client/base/tests.py b/lms/djangoapps/django_comment_client/base/tests.py index 43c0e56aaf..7b33deeb94 100644 --- a/lms/djangoapps/django_comment_client/base/tests.py +++ b/lms/djangoapps/django_comment_client/base/tests.py @@ -10,7 +10,6 @@ from django.test.client import RequestFactory from django.contrib.auth.models import User from django.core.management import call_command from django.core.urlresolvers import reverse -from request_cache.middleware import RequestCache from mock import patch, ANY, Mock from nose.tools import assert_true, assert_equal from nose.plugins.attrib import attr @@ -24,8 +23,9 @@ from django_comment_client.tests.utils import CohortedTestCase from django_comment_client.tests.unicode import UnicodeTestMixin from django_comment_common.models import Role from django_comment_common.utils import seed_permissions_roles, ThreadContext -from student.tests.factories import CourseEnrollmentFactory, UserFactory, CourseAccessRoleFactory + from lms.djangoapps.teams.tests.factories import CourseTeamFactory, CourseTeamMembershipFactory +from student.tests.factories import CourseEnrollmentFactory, UserFactory, CourseAccessRoleFactory from util.testing import UrlResetMixin from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase @@ -351,17 +351,12 @@ class ViewsTestCaseMixin(object): class ViewsQueryCountTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin, ViewsTestCaseMixin): CREATE_USER = False + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] @patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUp(self): super(ViewsQueryCountTestCase, self).setUp() - def clear_caches(self): - """Clears caches so that query count numbers are accurate.""" - for cache in settings.CACHES: - caches[cache].clear() - RequestCache.clear_request_cache() - def count_queries(func): # pylint: disable=no-self-argument """ Decorates test methods to count mongo and SQL calls for a diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index b5abbbb299..d1a095d869 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -51,7 +51,7 @@ from student.models import ( ) from student.tests.factories import UserFactory, CourseModeFactory, AdminFactory from student.roles import CourseBetaTesterRole, CourseSalesAdminRole, CourseFinanceAdminRole, CourseInstructorRole -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase, ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.fields import Date @@ -3968,27 +3968,6 @@ class TestDueDateExtensions(SharedModuleStoreTestCase, LoginEnrollmentTestCase): }) self.assertEqual(response.status_code, 400, response.content) - @SharedModuleStoreTestCase.modifies_courseware - def test_reset_extension_to_deleted_date(self): - """ - Test that we can delete a due date extension after deleting the normal - due date, without causing an error. - """ - self.test_change_due_date() - self.week1.due = None - self.week1 = self.store.update_item(self.week1, self.user1.id) - # Now, week1's normal due date is deleted but the extension still exists. - url = reverse('reset_due_date', kwargs={'course_id': self.course.id.to_deprecated_string()}) - response = self.client.get(url, { - 'student': self.user1.username, - 'url': self.week1.location.to_deprecated_string(), - }) - self.assertEqual(response.status_code, 200, response.content) - self.assertEqual( - None, - get_extended_due(self.course, self.week1, self.user1) - ) - def test_show_unit_extensions(self): self.test_change_due_date() url = reverse('show_unit_extensions', @@ -4017,6 +3996,114 @@ class TestDueDateExtensions(SharedModuleStoreTestCase, LoginEnrollmentTestCase): self.user1.profile.name, self.user1.username)}) +@attr('shard_1') +class TestDueDateExtensionsDeletedDate(ModuleStoreTestCase, LoginEnrollmentTestCase): + def setUp(self): + """ + Fixtures. + """ + super(TestDueDateExtensionsDeletedDate, self).setUp() + + self.course = CourseFactory.create() + self.due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc) + + with self.store.bulk_operations(self.course.id, emit_signals=False): + self.week1 = ItemFactory.create(due=self.due) + self.week2 = ItemFactory.create(due=self.due) + self.week3 = ItemFactory.create() # No due date + self.course.children = [ + self.week1.location.to_deprecated_string(), + self.week2.location.to_deprecated_string(), + self.week3.location.to_deprecated_string() + ] + self.homework = ItemFactory.create( + parent_location=self.week1.location, + due=self.due + ) + self.week1.children = [self.homework.location.to_deprecated_string()] + + user1 = UserFactory.create() + StudentModule( + state='{}', + student_id=user1.id, + course_id=self.course.id, + module_state_key=self.week1.location).save() + StudentModule( + state='{}', + student_id=user1.id, + course_id=self.course.id, + module_state_key=self.week2.location).save() + StudentModule( + state='{}', + student_id=user1.id, + course_id=self.course.id, + module_state_key=self.week3.location).save() + StudentModule( + state='{}', + student_id=user1.id, + course_id=self.course.id, + module_state_key=self.homework.location).save() + + user2 = UserFactory.create() + StudentModule( + state='{}', + student_id=user2.id, + course_id=self.course.id, + module_state_key=self.week1.location).save() + StudentModule( + state='{}', + student_id=user2.id, + course_id=self.course.id, + module_state_key=self.homework.location).save() + + user3 = UserFactory.create() + StudentModule( + state='{}', + student_id=user3.id, + course_id=self.course.id, + module_state_key=self.week1.location).save() + StudentModule( + state='{}', + student_id=user3.id, + course_id=self.course.id, + module_state_key=self.homework.location).save() + + self.user1 = user1 + self.user2 = user2 + self.instructor = InstructorFactory(course_key=self.course.id) + self.client.login(username=self.instructor.username, password='test') + + def test_reset_extension_to_deleted_date(self): + """ + Test that we can delete a due date extension after deleting the normal + due date, without causing an error. + """ + + url = reverse('change_due_date', kwargs={'course_id': self.course.id.to_deprecated_string()}) + response = self.client.get(url, { + 'student': self.user1.username, + 'url': self.week1.location.to_deprecated_string(), + 'due_datetime': '12/30/2013 00:00' + }) + self.assertEqual(response.status_code, 200, response.content) + self.assertEqual(datetime.datetime(2013, 12, 30, 0, 0, tzinfo=utc), + get_extended_due(self.course, self.week1, self.user1)) + + self.week1.due = None + self.week1 = self.store.update_item(self.week1, self.user1.id) + # Now, week1's normal due date is deleted but the extension still exists. + url = reverse('reset_due_date', kwargs={'course_id': self.course.id.to_deprecated_string()}) + response = self.client.get(url, { + 'student': self.user1.username, + 'url': self.week1.location.to_deprecated_string(), + }) + self.assertEqual(response.status_code, 200, response.content) + self.assertEqual( + None, + get_extended_due(self.course, self.week1, self.user1) + ) + + @attr('shard_1') class TestCourseIssuedCertificatesData(SharedModuleStoreTestCase): """ diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index a6238a824b..d5021027fd 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -1683,6 +1683,9 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase): """ Test certificate generation task works. """ + + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): super(TestCertificateGeneration, self).setUp() self.initialize_course() diff --git a/lms/djangoapps/mobile_api/tests/test_middleware.py b/lms/djangoapps/mobile_api/tests/test_middleware.py index 7ffbbdce50..86a675e495 100644 --- a/lms/djangoapps/mobile_api/tests/test_middleware.py +++ b/lms/djangoapps/mobile_api/tests/test_middleware.py @@ -10,18 +10,21 @@ import mock from pytz import UTC from mobile_api.middleware import AppVersionUpgrade from mobile_api.models import AppVersionConfig +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase @ddt.ddt -class TestAppVersionUpgradeMiddleware(TestCase): +class TestAppVersionUpgradeMiddleware(CacheIsolationTestCase): """ Tests for version based app upgrade middleware """ + + ENABLED_CACHES = ['default'] + def setUp(self): super(TestAppVersionUpgradeMiddleware, self).setUp() self.middleware = AppVersionUpgrade() self.set_app_version_config() - cache.clear() def set_app_version_config(self): """ Creates configuration data for platform versions """ diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index 175fce6622..15e2727077 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -1707,6 +1707,9 @@ class RegistrationCodeRedemptionCourseEnrollment(SharedModuleStoreTestCase): """ Test suite for RegistrationCodeRedemption Course Enrollments """ + + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + @classmethod def setUpClass(cls): super(RegistrationCodeRedemptionCourseEnrollment, cls).setUpClass() diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index 782b18b307..4482d54341 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -22,6 +22,7 @@ from course_modes.models import CourseMode from openedx.core.djangoapps.user_api.accounts.api import activate_account, create_account from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH from openedx.core.djangolib.js_utils import dump_js_escaped_json +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.tests.factories import UserFactory from student_account.views import account_settings_context from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin @@ -31,7 +32,7 @@ from openedx.core.djangoapps.theming.test_util import with_edx_domain_context @ddt.ddt -class StudentAccountUpdateTest(UrlResetMixin, TestCase): +class StudentAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin): """ Tests for the student account views that update the user's account information. """ USERNAME = u"heisenberg" @@ -64,6 +65,8 @@ class StudentAccountUpdateTest(UrlResetMixin, TestCase): URLCONF_MODULES = ['student_accounts.urls'] + ENABLED_CACHES = ['default'] + def setUp(self): super(StudentAccountUpdateTest, self).setUp() diff --git a/lms/djangoapps/verify_student/tests/test_fake_software_secure.py b/lms/djangoapps/verify_student/tests/test_fake_software_secure.py index f33b69a2a9..a9c2b4b8dc 100644 --- a/lms/djangoapps/verify_student/tests/test_fake_software_secure.py +++ b/lms/djangoapps/verify_student/tests/test_fake_software_secure.py @@ -14,7 +14,7 @@ class SoftwareSecureFakeViewTest(UrlResetMixin, TestCase): """ Base class to test the fake software secure view. """ - + URLCONF_MODULES = ['verify_student.urls'] def setUp(self, **kwargs): diff --git a/lms/djangoapps/verify_student/tests/test_models.py b/lms/djangoapps/verify_student/tests/test_models.py index 349cf7d144..b67732df0a 100644 --- a/lms/djangoapps/verify_student/tests/test_models.py +++ b/lms/djangoapps/verify_student/tests/test_models.py @@ -18,6 +18,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from opaque_keys.edx.keys import CourseKey +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from lms.djangoapps.verify_student.models import ( SoftwareSecurePhotoVerification, @@ -844,11 +845,13 @@ class SkippedReverificationTest(ModuleStoreTestCase): ) -class VerificationDeadlineTest(TestCase): +class VerificationDeadlineTest(CacheIsolationTestCase): """ Tests for the VerificationDeadline model. """ + ENABLED_CACHES = ['default'] + def test_caching(self): deadlines = { CourseKey.from_string("edX/DemoX/Fall"): datetime.now(pytz.UTC), diff --git a/lms/envs/test.py b/lms/envs/test.py index ba43f30574..99d2524735 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -203,9 +203,7 @@ CACHES = { # This is the cache used for most things. # In staging/prod envs, the sessions also live here. 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': 'edx_loc_mem_cache', - 'KEY_FUNCTION': 'util.memcache.safe_key', + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', }, # The general cache is what you get if you use our util.cache. It's used for @@ -215,20 +213,13 @@ CACHES = { # push process. 'general': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', - 'KEY_PREFIX': 'general', - 'VERSION': 4, - 'KEY_FUNCTION': 'util.memcache.safe_key', }, 'mongo_metadata_inheritance': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': os.path.join(tempfile.gettempdir(), 'mongo_metadata_inheritance'), - 'TIMEOUT': 300, - 'KEY_FUNCTION': 'util.memcache.safe_key', + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', }, 'loc_cache': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': 'edx_location_mem_cache', + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', }, 'course_structure_cache': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', diff --git a/openedx/core/djangoapps/bookmarks/tests/test_models.py b/openedx/core/djangoapps/bookmarks/tests/test_models.py index a2b762a2bb..e452a5a92e 100644 --- a/openedx/core/djangoapps/bookmarks/tests/test_models.py +++ b/openedx/core/djangoapps/bookmarks/tests/test_models.py @@ -40,6 +40,8 @@ class BookmarksTestsBase(ModuleStoreTestCase): STORE_TYPE = ModuleStoreEnum.Type.mongo TEST_PASSWORD = 'test' + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): super(BookmarksTestsBase, self).setUp() diff --git a/openedx/core/djangoapps/content/course_structures/api/v0/tests_api.py b/openedx/core/djangoapps/content/course_structures/api/v0/tests_api.py index 927c3698c1..ce1093dbea 100644 --- a/openedx/core/djangoapps/content/course_structures/api/v0/tests_api.py +++ b/openedx/core/djangoapps/content/course_structures/api/v0/tests_api.py @@ -16,6 +16,8 @@ class CourseStructureApiTests(ModuleStoreTestCase): """ MOCK_CACHE = "openedx.core.djangoapps.content.course_structures.api.v0.api.cache" + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): """ Test setup diff --git a/openedx/core/djangoapps/credentials/tests/test_utils.py b/openedx/core/djangoapps/credentials/tests/test_utils.py index 8de680f79f..0d68827e9c 100644 --- a/openedx/core/djangoapps/credentials/tests/test_utils.py +++ b/openedx/core/djangoapps/credentials/tests/test_utils.py @@ -16,16 +16,20 @@ from openedx.core.djangoapps.credentials.utils import ( ) from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin from openedx.core.djangoapps.programs.models import ProgramsApiConfig +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.tests.factories import UserFactory @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @attr('shard_2') class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin, CredentialsDataMixin, - ProgramsDataMixin, TestCase): + ProgramsDataMixin, CacheIsolationTestCase): """ Tests covering the retrieval of user credentials from the Credentials service. """ + + ENABLED_CACHES = ['default'] + def setUp(self): super(TestCredentialsRetrieval, self).setUp() diff --git a/openedx/core/djangoapps/credit/tests/test_api.py b/openedx/core/djangoapps/credit/tests/test_api.py index d68fe77b6a..99f4b363c6 100644 --- a/openedx/core/djangoapps/credit/tests/test_api.py +++ b/openedx/core/djangoapps/credit/tests/test_api.py @@ -55,6 +55,8 @@ class CreditApiTestBase(ModuleStoreTestCase): Base class for test cases of the credit API. """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + PROVIDER_ID = "hogwarts" PROVIDER_NAME = "Hogwarts School of Witchcraft and Wizardry" PROVIDER_URL = "https://credit.example.com/request" diff --git a/openedx/core/djangoapps/credit/tests/test_partition.py b/openedx/core/djangoapps/credit/tests/test_partition.py index 96fe676ac1..2397f20764 100644 --- a/openedx/core/djangoapps/credit/tests/test_partition.py +++ b/openedx/core/djangoapps/credit/tests/test_partition.py @@ -31,6 +31,7 @@ class ReverificationPartitionTest(ModuleStoreTestCase): SUBMITTED = "submitted" APPROVED = "approved" DENIED = "denied" + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] def setUp(self): super(ReverificationPartitionTest, self).setUp() diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index 19a6638c80..245a9703b2 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -17,6 +17,7 @@ from openedx.core.djangoapps.programs import utils from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.tests import factories from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.tests.factories import UserFactory, CourseEnrollmentFactory @@ -26,8 +27,11 @@ UTILS_MODULE = 'openedx.core.djangoapps.programs.utils' @skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @attr('shard_2') class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, - CredentialsApiConfigMixin, TestCase): + CredentialsApiConfigMixin, CacheIsolationTestCase): """Tests covering the retrieval of programs from the Programs service.""" + + ENABLED_CACHES = ['default'] + def setUp(self): super(TestProgramRetrieval, self).setUp() diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py index 2cd04ea5d2..5ebe13efe5 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py @@ -25,6 +25,7 @@ from student.tests.factories import UserFactory from student.models import UserProfile, LanguageProficiency, PendingEmailChange from openedx.core.djangoapps.user_api.accounts import ACCOUNT_VISIBILITY_PREF_KEY from openedx.core.djangoapps.user_api.preferences.api import set_user_preference +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from .. import PRIVATE_VISIBILITY, ALL_USERS_VISIBILITY TEST_PROFILE_IMAGE_UPLOADED_AT = datetime.datetime(2002, 1, 9, 15, 43, 01, tzinfo=UTC) @@ -121,10 +122,13 @@ class UserAPITestCase(APITestCase): clear=True ) @attr('shard_2') -class TestAccountAPI(UserAPITestCase): +class TestAccountAPI(CacheIsolationTestCase, UserAPITestCase): """ Unit tests for the Account API. """ + + ENABLED_CACHES = ['default'] + def setUp(self): super(TestAccountAPI, self).setUp() @@ -244,7 +248,7 @@ class TestAccountAPI(UserAPITestCase): """ self.different_client.login(username=self.different_user.username, password=self.test_password) self.create_mock_profile(self.user) - with self.assertNumQueries(11): + with self.assertNumQueries(14): response = self.send_get(self.different_client) self._verify_full_shareable_account_response(response, account_privacy=ALL_USERS_VISIBILITY) @@ -259,7 +263,7 @@ class TestAccountAPI(UserAPITestCase): """ self.different_client.login(username=self.different_user.username, password=self.test_password) self.create_mock_profile(self.user) - with self.assertNumQueries(11): + with self.assertNumQueries(14): response = self.send_get(self.different_client) self._verify_private_account_response(response, account_privacy=PRIVATE_VISIBILITY) @@ -307,11 +311,11 @@ class TestAccountAPI(UserAPITestCase): Test that a client (logged in) can get her own account information (using default legacy profile information, as created by the test UserFactory). """ - def verify_get_own_information(): + def verify_get_own_information(queries): """ Internal helper to perform the actual assertions """ - with self.assertNumQueries(9): + with self.assertNumQueries(queries): response = self.send_get(self.client) data = response.data self.assertEqual(17, len(data)) @@ -333,12 +337,12 @@ class TestAccountAPI(UserAPITestCase): self.assertEqual(False, data["accomplishments_shared"]) self.client.login(username=self.user.username, password=self.test_password) - verify_get_own_information() + verify_get_own_information(12) # Now make sure that the user can get the same information, even if not active self.user.is_active = False self.user.save() - verify_get_own_information() + verify_get_own_information(9) def test_get_account_empty_string(self): """ @@ -352,7 +356,7 @@ class TestAccountAPI(UserAPITestCase): legacy_profile.save() self.client.login(username=self.user.username, password=self.test_password) - with self.assertNumQueries(9): + with self.assertNumQueries(12): response = self.send_get(self.client) for empty_field in ("level_of_education", "gender", "country", "bio"): self.assertIsNone(response.data[empty_field]) diff --git a/openedx/core/djangoapps/user_api/tests/test_views.py b/openedx/core/djangoapps/user_api/tests/test_views.py index ad9177e19c..fb28687896 100644 --- a/openedx/core/djangoapps/user_api/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/tests/test_views.py @@ -20,6 +20,7 @@ from social.apps.django_app.default.models import UserSocialAuth from django_comment_common import models from openedx.core.lib.api.test_utils import ApiTestCase, TEST_API_KEY +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.tests.factories import UserFactory from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin from third_party_auth.tests.utils import ( @@ -334,12 +335,14 @@ class UserViewSetTest(UserApiTestCase): ) -class UserPreferenceViewSetTest(UserApiTestCase): +class UserPreferenceViewSetTest(CacheIsolationTestCase, UserApiTestCase): """ Test cases covering the User Preference DRF view class and its various behaviors """ LIST_URI = USER_PREFERENCE_LIST_URI + ENABLED_CACHES = ['default'] + def setUp(self): super(UserPreferenceViewSetTest, self).setUp() self.detail_uri = self.get_uri_for_pref(self.prefs[0]) @@ -1725,12 +1728,16 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase): @httpretty.activate @ddt.ddt -class ThirdPartyRegistrationTestMixin(ThirdPartyOAuthTestMixin): +class ThirdPartyRegistrationTestMixin(ThirdPartyOAuthTestMixin, CacheIsolationTestCase): """ Tests for the User API registration endpoint with 3rd party authentication. """ CREATE_USER = False + ENABLED_CACHES = ['default'] + + __test__ = False + def setUp(self): super(ThirdPartyRegistrationTestMixin, self).setUp() self.url = reverse('user_api_registration') @@ -1847,6 +1854,8 @@ class TestFacebookRegistrationView( ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinFacebook, TransactionTestCase ): """Tests the User API registration endpoint with Facebook authentication.""" + __test__ = True + def test_social_auth_exception(self): """ According to the do_auth method in social.backends.facebook.py, @@ -1863,6 +1872,8 @@ class TestGoogleRegistrationView( ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinGoogle, TransactionTestCase ): """Tests the User API registration endpoint with Google authentication.""" + __test__ = True + pass diff --git a/openedx/core/djangolib/testing/__init__.py b/openedx/core/djangolib/testing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/core/djangolib/testing/utils.py b/openedx/core/djangolib/testing/utils.py new file mode 100644 index 0000000000..4feb1749c3 --- /dev/null +++ b/openedx/core/djangolib/testing/utils.py @@ -0,0 +1,122 @@ +from django.core.cache import caches +from django.test import TestCase, override_settings +from django.conf import settings +from django.contrib import sites + +from request_cache.middleware import RequestCache + + +class CacheIsolationMixin(object): + """ + This class can be used to enable specific django caches for + specific the TestCase that it's mixed into. + + Usage: + + Use the ENABLED_CACHES to list the names of caches that should + be enabled in the context of this TestCase. These caches will + use a loc_mem_cache with the default settings. + + Set the class variable CACHES to explicitly specify the cache settings + that should be overridden. This class will insert those values into + django.conf.settings, and will reset all named caches before each + test. + + If both CACHES and ENABLED_CACHES are not None, raises an error. + """ + + CACHES = None + ENABLED_CACHES = None + __settings_override = None + + @classmethod + def start_cache_isolation(cls): + """ + Start cache isolation by overriding the settings.CACHES and + flushing the cache. + """ + cache_settings = None + if cls.CACHES is not None and cls.ENABLED_CACHES is not None: + raise Exception( + "Use either CACHES or ENABLED_CACHES, but not both" + ) + + if cls.CACHES is not None: + cache_settings = cls.CACHES + elif cls.ENABLED_CACHES is not None: + cache_settings = { + 'default': { + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + } + } + + cache_settings.update({ + cache_name: { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': cache_name, + 'KEY_FUNCTION': 'util.memcache.safe_key', + } for cache_name in cls.ENABLED_CACHES + }) + + if cache_settings is None: + return + + cls.__settings_override = override_settings(CACHES=cache_settings) + cls.__settings_override.__enter__() + + # Start with empty caches + cls.clear_caches() + + @classmethod + def end_cache_isolation(cls): + """ + End cache isolation by flushing the cache and then returning + settings.CACHES to its original state. + """ + # Make sure that cache contents don't leak out after the isolation is ended + cls.clear_caches() + + if cls.__settings_override is not None: + cls.__settings_override.__exit__(None, None, None) + cls.__settings_override = None + + @classmethod + def clear_caches(cls): + """ + Clear all of the caches defined in settings.CACHES. + """ + # N.B. As of 2016-04-20, Django won't return any caches + # from django.core.cache.caches.all() that haven't been + # accessed using caches[name] previously, so we loop + # over our list of overridden caches, instead. + for cache in settings.CACHES: + caches[cache].clear() + + # The sites framework caches in a module-level dictionary. + # Clear that. + sites.models.SITE_CACHE.clear() + + RequestCache.clear_request_cache() + + +class CacheIsolationTestCase(CacheIsolationMixin, TestCase): + """ + A TestCase that isolates caches (as described in + :py:class:`CacheIsolationMixin`) at class setup, and flushes the cache + between every test. + """ + @classmethod + def setUpClass(cls): + super(CacheIsolationTestCase, cls).setUpClass() + cls.start_cache_isolation() + + @classmethod + def tearDownClass(cls): + cls.end_cache_isolation() + super(CacheIsolationTestCase, cls).tearDownClass() + + def setUp(self): + super(CacheIsolationTestCase, self).setUp() + + self.clear_caches() + self.addCleanup(self.clear_caches) diff --git a/openedx/core/lib/tests/test_edx_api_utils.py b/openedx/core/lib/tests/test_edx_api_utils.py index 1570c0b4bb..76b6b8b563 100644 --- a/openedx/core/lib/tests/test_edx_api_utils.py +++ b/openedx/core/lib/tests/test_edx_api_utils.py @@ -15,6 +15,7 @@ from openedx.core.djangoapps.credentials.models import CredentialsApiConfig from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from openedx.core.lib.edx_api_utils import get_edx_api_data from student.tests.factories import UserFactory @@ -24,8 +25,11 @@ LOGGER_NAME = 'openedx.core.lib.edx_api_utils' @attr('shard_2') class TestApiDataRetrieval(CredentialsApiConfigMixin, CredentialsDataMixin, ProgramsApiConfigMixin, ProgramsDataMixin, - TestCase): + CacheIsolationTestCase): """Test utility for API data retrieval.""" + + ENABLED_CACHES = ['default'] + def setUp(self): super(TestApiDataRetrieval, self).setUp() ClientFactory(name=CredentialsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)