Files
edx-platform/common/djangoapps/embargo/tests/test_middleware.py
Calen Pennington b353ed2ea2 Better support specifying of modulestore configuration in test cases
The existing pattern of using `override_settings(MODULESTORE=...)` prevented
us from having more than one layer of subclassing in modulestore tests.

In a structure like:

    @override_settings(MODULESTORE=store_a)
    class BaseTestCase(ModuleStoreTestCase):
        def setUp(self):
            # use store

    @override_settings(MODULESTORE=store_b)
    class ChildTestCase(BaseTestCase):
        def setUp(self):
            # use store

In this case, the store actions performed in `BaseTestCase` on behalf of
`ChildTestCase` would still use `store_a`, even though the `ChildTestCase`
had specified to use `store_b`. This is because the `override_settings`
decorator would be the innermost wrapper around the `BaseTestCase.setUp` method,
no matter what `ChildTestCase` does.

To remedy this, we move the call to `override_settings` into the
`ModuleStoreTestCase.setUp` method, and use a cleanup to remove the override.
Subclasses can just defined the `MODULESTORE` class attribute to specify which
modulestore to use _for the entire `setUp` chain_.

[PLAT-419]
2015-02-04 09:09:14 -05:00

333 lines
15 KiB
Python

"""
Tests for EmbargoMiddleware
"""
import mock
import pygeoip
import unittest
from django.core.urlresolvers import reverse
from django.conf import settings
from django.db import connection, transaction
from django.test.utils import override_settings
import ddt
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, mixed_store_config
)
# Explicitly import the cache from ConfigurationModel so we can reset it after each test
from config_models.models import cache
from embargo.models import EmbargoedCourse, EmbargoedState, IPFilter
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EmbargoMiddlewareTests(ModuleStoreTestCase):
"""
Tests of EmbargoMiddleware
"""
def setUp(self):
super(EmbargoMiddlewareTests, self).setUp()
self.user = UserFactory(username='fred', password='secret')
self.client.login(username='fred', password='secret')
self.embargo_course = CourseFactory.create()
self.embargo_course.save()
self.regular_course = CourseFactory.create(org="Regular")
self.regular_course.save()
self.embargoed_page = '/courses/' + self.embargo_course.id.to_deprecated_string() + '/info'
self.regular_page = '/courses/' + self.regular_course.id.to_deprecated_string() + '/info'
EmbargoedCourse(course_id=self.embargo_course.id, embargoed=True).save()
EmbargoedState(
embargoed_countries="cu, ir, Sy, SD",
changed_by=self.user,
enabled=True
).save()
CourseEnrollment.enroll(self.user, self.regular_course.id)
CourseEnrollment.enroll(self.user, self.embargo_course.id)
# Text from lms/templates/static_templates/embargo.html
self.embargo_text = "Unfortunately, at this time edX must comply with export controls, and we cannot allow you to access this course."
self.patcher = mock.patch.object(pygeoip.GeoIP, 'country_code_by_addr', self.mock_country_code_by_addr)
self.patcher.start()
def tearDown(self):
# Explicitly clear ConfigurationModel's cache so tests have a clear cache
# and don't interfere with each other
cache.clear()
self.patcher.stop()
def mock_country_code_by_addr(self, ip_addr):
"""
Gives us a fake set of IPs
"""
ip_dict = {
'1.0.0.0': 'CU',
'2.0.0.0': 'IR',
'3.0.0.0': 'SY',
'4.0.0.0': 'SD',
'5.0.0.0': 'AQ', # Antartica
'2001:250::': 'CN',
'2001:1340::': 'CU',
}
return ip_dict.get(ip_addr, 'US')
def test_countries(self):
# Accessing an embargoed page from a blocked IP should cause a redirect
response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0')
self.assertEqual(response.status_code, 302)
# Following the redirect should give us the embargo page
response = self.client.get(
self.embargoed_page,
HTTP_X_FORWARDED_FOR='1.0.0.0',
REMOTE_ADDR='1.0.0.0',
follow=True
)
self.assertIn(self.embargo_text, response.content)
# Accessing a regular page from a blocked IP should succeed
response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0')
self.assertEqual(response.status_code, 200)
# Accessing an embargoed page from a non-embargoed IP should succeed
response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0')
self.assertEqual(response.status_code, 200)
# Accessing a regular page from a non-embargoed IP should succeed
response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0')
self.assertEqual(response.status_code, 200)
def test_countries_ipv6(self):
# Accessing an embargoed page from a blocked IP should cause a redirect
response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='2001:1340::', REMOTE_ADDR='2001:1340::')
self.assertEqual(response.status_code, 302)
# Following the redirect should give us the embargo page
response = self.client.get(
self.embargoed_page,
HTTP_X_FORWARDED_FOR='2001:1340::',
REMOTE_ADDR='2001:1340::',
follow=True
)
self.assertIn(self.embargo_text, response.content)
# Accessing a regular page from a blocked IP should succeed
response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='2001:1340::', REMOTE_ADDR='2001:1340::')
self.assertEqual(response.status_code, 200)
# Accessing an embargoed page from a non-embargoed IP should succeed
response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='2001:250::', REMOTE_ADDR='2001:250::')
self.assertEqual(response.status_code, 200)
# Accessing a regular page from a non-embargoed IP should succeed
response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='2001:250::', REMOTE_ADDR='2001:250::')
self.assertEqual(response.status_code, 200)
def test_ip_exceptions(self):
# Explicitly whitelist/blacklist some IPs
IPFilter(
whitelist='1.0.0.0',
blacklist='5.0.0.0',
changed_by=self.user,
enabled=True
).save()
# Accessing an embargoed page from a blocked IP that's been whitelisted
# should succeed
response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0')
self.assertEqual(response.status_code, 200)
# Accessing a regular course from a blocked IP that's been whitelisted should succeed
response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0')
self.assertEqual(response.status_code, 200)
# Accessing an embargoed course from non-embargoed IP that's been blacklisted
# should cause a redirect
response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0')
self.assertEqual(response.status_code, 302)
# Following the redirect should give us the embargo page
response = self.client.get(
self.embargoed_page,
HTTP_X_FORWARDED_FOR='5.0.0.0',
REMOTE_ADDR='1.0.0.0',
follow=True
)
self.assertIn(self.embargo_text, response.content)
# Accessing a regular course from a non-embargoed IP that's been blacklisted should succeed
response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0')
self.assertEqual(response.status_code, 200)
def test_ip_network_exceptions(self):
# Explicitly whitelist/blacklist some IP networks
IPFilter(
whitelist='1.0.0.1/24',
blacklist='5.0.0.0/16,1.1.0.0/24',
changed_by=self.user,
enabled=True
).save()
# Accessing an embargoed page from a blocked IP that's been whitelisted with a network
# should succeed
response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0')
self.assertEqual(response.status_code, 200)
# Accessing a regular course from a blocked IP that's been whitelisted with a network
# should succeed
response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0')
self.assertEqual(response.status_code, 200)
# Accessing an embargoed course from non-embargoed IP that's been blacklisted with a network
# should cause a redirect
response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='5.0.0.100', REMOTE_ADDR='5.0.0.100')
self.assertEqual(response.status_code, 302)
# Following the redirect should give us the embargo page
response = self.client.get(
self.embargoed_page,
HTTP_X_FORWARDED_FOR='5.0.0.100',
REMOTE_ADDR='5.0.0.100',
follow=True
)
self.assertIn(self.embargo_text, response.content)
# Accessing an embargoed course from non-embargoed IP that's been blaclisted with a network
# should cause a redirect
response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='1.1.0.1', REMOTE_ADDR='1.1.0.1')
self.assertEqual(response.status_code, 302)
# Following the redirect should give us the embargo page
response = self.client.get(
self.embargoed_page,
HTTP_X_FORWARDED_FOR='1.1.0.0',
REMOTE_ADDR='1.1.0.0',
follow=True
)
self.assertIn(self.embargo_text, response.content)
# Accessing an embargoed from a blocked IP that's not blacklisted by the network rule.
# should succeed
response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='1.1.1.0', REMOTE_ADDR='1.1.1.0')
self.assertEqual(response.status_code, 200)
# Accessing a regular course from a non-embargoed IP that's been blacklisted
# should succeed
response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0')
self.assertEqual(response.status_code, 200)
@ddt.data(
(None, False),
("", False),
("us", False),
("CU", True),
("Ir", True),
("sy", True),
("sd", True)
)
@ddt.unpack
def test_embargo_profile_country(self, profile_country, is_embargoed):
# Set the country in the user's profile
profile = self.user.profile
profile.country = profile_country
profile.save()
# Attempt to access an embargoed course
response = self.client.get(self.embargoed_page)
# If the user is from an embargoed country, verify that
# they are redirected to the embargo page.
if is_embargoed:
embargo_url = reverse('embargo')
self.assertRedirects(response, embargo_url)
# Otherwise, verify that the student can access the page
else:
self.assertEqual(response.status_code, 200)
# For non-embargoed courses, the student should be able to access
# the page, even if he/she is from an embargoed country.
response = self.client.get(self.regular_page)
self.assertEqual(response.status_code, 200)
def test_embargo_profile_country_cache(self):
# Set the country in the user's profile
profile = self.user.profile
profile.country = "us"
profile.save()
# Warm the cache
with self.assertNumQueries(16):
self.client.get(self.embargoed_page)
# Access the page multiple times, but expect that we hit
# the database to check the user's profile only once
with self.assertNumQueries(10):
self.client.get(self.embargoed_page)
def test_embargo_profile_country_db_null(self):
# Django country fields treat NULL values inconsistently.
# When saving a profile with country set to None, Django saves an empty string to the database.
# However, when the country field loads a NULL value from the database, it sets
# `country.code` to `None`. This caused a bug in which country values created by
# the original South schema migration -- which defaulted to NULL -- caused a runtime
# exception when the embargo middleware treated the value as a string.
# In order to simulate this behavior, we can't simply set `profile.country = None`.
# (because when we save it, it will set the database field to an empty string instead of NULL)
query = "UPDATE auth_userprofile SET country = NULL WHERE id = %s"
connection.cursor().execute(query, [str(self.user.profile.id)])
transaction.commit_unless_managed()
# Attempt to access an embargoed course
# Verify that the student can access the page without an error
response = self.client.get(self.embargoed_page)
self.assertEqual(response.status_code, 200)
@mock.patch.dict(settings.FEATURES, {'EMBARGO': False})
def test_countries_embargo_off(self):
# When the middleware is turned off, all requests should go through
# Accessing an embargoed page from a blocked IP OK
response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0')
self.assertEqual(response.status_code, 200)
# Accessing a regular page from a blocked IP should succeed
response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0')
self.assertEqual(response.status_code, 200)
# Explicitly whitelist/blacklist some IPs
IPFilter(
whitelist='1.0.0.0',
blacklist='5.0.0.0',
changed_by=self.user,
enabled=True
).save()
# Accessing an embargoed course from non-embargoed IP that's been blacklisted
# should be OK
response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0')
self.assertEqual(response.status_code, 200)
# Accessing a regular course from a non-embargoed IP that's been blacklisted should succeed
response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0')
self.assertEqual(response.status_code, 200)
@mock.patch.dict(settings.FEATURES, {'EMBARGO': False, 'SITE_EMBARGOED': True})
def test_embargo_off_embargo_site_on(self):
# When the middleware is turned on with SITE, main site access should be restricted
# Accessing a regular page from a blocked IP is denied.
response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0')
self.assertEqual(response.status_code, 403)
# Accessing a regular page from a non blocked IP should succeed
response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0')
self.assertEqual(response.status_code, 200)
@mock.patch.dict(settings.FEATURES, {'EMBARGO': False, 'SITE_EMBARGOED': True})
@override_settings(EMBARGO_SITE_REDIRECT_URL='https://www.edx.org/')
def test_embargo_off_embargo_site_on_with_redirect_url(self):
# When the middleware is turned on with SITE_EMBARGOED, main site access
# should be restricted. Accessing a regular page from a blocked IP is
# denied, and redirected to EMBARGO_SITE_REDIRECT_URL rather than returning a 403.
response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0')
self.assertEqual(response.status_code, 302)