Merge pull request #13161 from mitocw/fix/aq/support_django_oauth_toolkit
Fixed authentication classes to support Django OAUTH Toolkit
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
"""
|
||||
Tests for the CCX REST APIs.
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import math
|
||||
import pytz
|
||||
import string
|
||||
import urllib
|
||||
import urlparse
|
||||
from datetime import datetime, timedelta
|
||||
from itertools import izip
|
||||
|
||||
import ddt
|
||||
@@ -20,6 +20,7 @@ from django.core.urlresolvers import (
|
||||
Resolver404
|
||||
)
|
||||
from nose.plugins.attrib import attr
|
||||
from oauth2_provider import models as dot_models
|
||||
from provider.constants import CONFIDENTIAL
|
||||
from provider.oauth2.models import (
|
||||
Client,
|
||||
@@ -52,6 +53,10 @@ from student.roles import (
|
||||
from student.tests.factories import AdminFactory
|
||||
|
||||
|
||||
USER_PASSWORD = 'test'
|
||||
AUTH_ATTRS = ('auth', 'auth_header_oauth2_provider')
|
||||
|
||||
|
||||
class CcxRestApiTest(CcxTestCase, APITestCase):
|
||||
"""
|
||||
Base class with common methods to be used in the test classes of this module
|
||||
@@ -70,7 +75,12 @@ class CcxRestApiTest(CcxTestCase, APITestCase):
|
||||
self.master_course_key_str = unicode(self.master_course_key)
|
||||
# OAUTH2 setup
|
||||
# create a specific user for the application
|
||||
app_user = UserFactory(username='test_app_user', email='test_app_user@openedx.org', password='test')
|
||||
self.app_user = app_user = UserFactory(
|
||||
username='test_app_user',
|
||||
email='test_app_user@openedx.org',
|
||||
password=USER_PASSWORD
|
||||
)
|
||||
|
||||
# add staff role to the app user
|
||||
CourseStaffRole(self.master_course_key).add_users(app_user)
|
||||
|
||||
@@ -78,35 +88,22 @@ class CcxRestApiTest(CcxTestCase, APITestCase):
|
||||
instructor = UserFactory()
|
||||
allow_access(self.course, instructor, 'instructor')
|
||||
|
||||
# create an oauth client app entry
|
||||
self.app_client = Client.objects.create(
|
||||
user=app_user,
|
||||
name='test client',
|
||||
url='http://localhost//',
|
||||
redirect_uri='http://localhost//',
|
||||
client_type=CONFIDENTIAL
|
||||
)
|
||||
# create an authorization code
|
||||
self.app_grant = Grant.objects.create(
|
||||
user=app_user,
|
||||
client=self.app_client,
|
||||
redirect_uri='http://localhost//'
|
||||
)
|
||||
self.auth, self.auth_header_oauth2_provider = self.prepare_auth_token(app_user)
|
||||
|
||||
self.course.enable_ccx = True
|
||||
self.mstore.update_item(self.course, self.coach.id)
|
||||
self.auth = self.get_auth_token()
|
||||
# making the master course chapters easily available
|
||||
self.master_course_chapters = get_course_chapters(self.master_course_key)
|
||||
|
||||
def get_auth_token(self):
|
||||
def get_auth_token(self, app_grant, app_client):
|
||||
"""
|
||||
Helper method to get the oauth token
|
||||
"""
|
||||
token_data = {
|
||||
'grant_type': 'authorization_code',
|
||||
'code': self.app_grant.code,
|
||||
'client_id': self.app_client.client_id,
|
||||
'client_secret': self.app_client.client_secret
|
||||
'code': app_grant.code,
|
||||
'client_id': app_client.client_id,
|
||||
'client_secret': app_client.client_secret
|
||||
}
|
||||
token_resp = self.client.post('/oauth2/access_token/', data=token_data)
|
||||
self.assertEqual(token_resp.status_code, status.HTTP_200_OK)
|
||||
@@ -116,6 +113,47 @@ class CcxRestApiTest(CcxTestCase, APITestCase):
|
||||
token=token_resp_json['access_token']
|
||||
)
|
||||
|
||||
def prepare_auth_token(self, user):
|
||||
"""
|
||||
creates auth token for users
|
||||
"""
|
||||
# create an oauth client app entry
|
||||
app_client = Client.objects.create(
|
||||
user=user,
|
||||
name='test client',
|
||||
url='http://localhost//',
|
||||
redirect_uri='http://localhost//',
|
||||
client_type=CONFIDENTIAL
|
||||
)
|
||||
# create an authorization code
|
||||
app_grant = Grant.objects.create(
|
||||
user=user,
|
||||
client=app_client,
|
||||
redirect_uri='http://localhost//'
|
||||
)
|
||||
|
||||
# create an oauth2 provider client app entry
|
||||
app_client_oauth2_provider = dot_models.Application.objects.create(
|
||||
name='test client 2',
|
||||
user=user,
|
||||
client_type='confidential',
|
||||
authorization_grant_type='authorization-code',
|
||||
redirect_uris='http://localhost:8079/complete/edxorg/'
|
||||
)
|
||||
# create an authorization code
|
||||
auth_oauth2_provider = dot_models.AccessToken.objects.create(
|
||||
user=user,
|
||||
application=app_client_oauth2_provider,
|
||||
expires=datetime.utcnow() + timedelta(weeks=1),
|
||||
scope='read write',
|
||||
token='16MGyP3OaQYHmpT1lK7Q6MMNAZsjwF'
|
||||
)
|
||||
|
||||
auth_header_oauth2_provider = "Bearer {0}".format(auth_oauth2_provider)
|
||||
auth = self.get_auth_token(app_grant, app_client)
|
||||
|
||||
return auth, auth_header_oauth2_provider
|
||||
|
||||
def expect_error(self, http_code, error_code_str, resp_obj):
|
||||
"""
|
||||
Helper function that checks that the response object
|
||||
@@ -160,7 +198,8 @@ class CcxListTest(CcxRestApiTest):
|
||||
'?master_course_id={0}'.format(urllib.quote_plus(self.master_course_key_str))
|
||||
)
|
||||
|
||||
def test_authorization(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_authorization(self, auth_attr):
|
||||
"""
|
||||
Test that only the right token is authorized
|
||||
"""
|
||||
@@ -175,7 +214,8 @@ class CcxListTest(CcxRestApiTest):
|
||||
for auth in auth_list:
|
||||
resp = self.client.get(self.list_url_master_course, {}, HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
resp = self.client.get(self.list_url_master_course, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
|
||||
resp = self.client.get(self.list_url_master_course, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_authorization_no_oauth_staff(self):
|
||||
@@ -183,7 +223,11 @@ class CcxListTest(CcxRestApiTest):
|
||||
Check authorization for staff users logged in without oauth
|
||||
"""
|
||||
# create a staff user
|
||||
staff_user = UserFactory(username='test_staff_user', email='test_staff_user@openedx.org', password='test')
|
||||
staff_user = UserFactory(
|
||||
username='test_staff_user',
|
||||
email='test_staff_user@openedx.org',
|
||||
password=USER_PASSWORD
|
||||
)
|
||||
# add staff role to the staff user
|
||||
CourseStaffRole(self.master_course_key).add_users(staff_user)
|
||||
|
||||
@@ -194,7 +238,7 @@ class CcxListTest(CcxRestApiTest):
|
||||
'coach_email': self.coach.email
|
||||
}
|
||||
# the staff user can perform the request
|
||||
self.client.login(username=staff_user.username, password='test')
|
||||
self.client.login(username=staff_user.username, password=USER_PASSWORD)
|
||||
resp = self.client.get(self.list_url_master_course)
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
resp = self.client.post(self.list_url, data, format='json')
|
||||
@@ -206,7 +250,7 @@ class CcxListTest(CcxRestApiTest):
|
||||
"""
|
||||
# create an instructor user
|
||||
instructor_user = UserFactory(
|
||||
username='test_instructor_user', email='test_instructor_user@openedx.org', password='test'
|
||||
username='test_instructor_user', email='test_instructor_user@openedx.org', password=USER_PASSWORD
|
||||
)
|
||||
# add instructor role to the instructor user
|
||||
CourseInstructorRole(self.master_course_key).add_users(instructor_user)
|
||||
@@ -219,7 +263,7 @@ class CcxListTest(CcxRestApiTest):
|
||||
}
|
||||
|
||||
# the instructor user can perform the request
|
||||
self.client.login(username=instructor_user.username, password='test')
|
||||
self.client.login(username=instructor_user.username, password=USER_PASSWORD)
|
||||
resp = self.client.get(self.list_url_master_course)
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
resp = self.client.post(self.list_url, data, format='json')
|
||||
@@ -231,7 +275,7 @@ class CcxListTest(CcxRestApiTest):
|
||||
"""
|
||||
# create an coach user
|
||||
coach_user = UserFactory(
|
||||
username='test_coach_user', email='test_coach_user@openedx.org', password='test'
|
||||
username='test_coach_user', email='test_coach_user@openedx.org', password=USER_PASSWORD
|
||||
)
|
||||
# add coach role to the coach user
|
||||
CourseCcxCoachRole(self.master_course_key).add_users(coach_user)
|
||||
@@ -243,13 +287,14 @@ class CcxListTest(CcxRestApiTest):
|
||||
'coach_email': self.coach.email
|
||||
}
|
||||
# the coach user cannot perform the request: this type of user can only get her own CCX
|
||||
self.client.login(username=coach_user.username, password='test')
|
||||
self.client.login(username=coach_user.username, password=USER_PASSWORD)
|
||||
resp = self.client.get(self.list_url_master_course)
|
||||
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||
resp = self.client.post(self.list_url, data, format='json')
|
||||
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_get_list_wrong_master_course(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_get_list_wrong_master_course(self, auth_attr):
|
||||
"""
|
||||
Test for various get requests with wrong master course string
|
||||
"""
|
||||
@@ -258,27 +303,31 @@ class CcxListTest(CcxRestApiTest):
|
||||
with mock.patch(mock_class_str, autospec=True) as mocked_perm_class:
|
||||
mocked_perm_class.return_value = True
|
||||
# case with no master_course_id provided
|
||||
resp = self.client.get(self.list_url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.get(self.list_url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_400_BAD_REQUEST, 'master_course_id_not_provided', resp)
|
||||
|
||||
base_url = urlparse.urljoin(self.list_url, '?master_course_id=')
|
||||
# case with empty master_course_id
|
||||
resp = self.client.get(base_url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.get(base_url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_400_BAD_REQUEST, 'course_id_not_valid', resp)
|
||||
|
||||
# case with invalid master_course_id
|
||||
url = '{0}invalid_master_course_str'.format(base_url)
|
||||
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_400_BAD_REQUEST, 'course_id_not_valid', resp)
|
||||
|
||||
# case with inexistent master_course_id
|
||||
url = '{0}course-v1%3Aorg_foo.0%2Bcourse_bar_0%2BRun_0'.format(base_url)
|
||||
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_404_NOT_FOUND, 'course_id_does_not_exist', resp)
|
||||
|
||||
def test_get_list(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_get_list(self, auth_attr):
|
||||
"""
|
||||
Tests the API to get a list of CCX Courses
|
||||
"""
|
||||
# there are no CCX courses
|
||||
resp = self.client.get(self.list_url_master_course, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.get(self.list_url_master_course, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertIn('count', resp.data) # pylint: disable=no-member
|
||||
self.assertEqual(resp.data['count'], 0) # pylint: disable=no-member
|
||||
|
||||
@@ -286,14 +335,15 @@ class CcxListTest(CcxRestApiTest):
|
||||
num_ccx = 10
|
||||
for _ in xrange(num_ccx):
|
||||
self.make_ccx()
|
||||
resp = self.client.get(self.list_url_master_course, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.get(self.list_url_master_course, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('count', resp.data) # pylint: disable=no-member
|
||||
self.assertEqual(resp.data['count'], num_ccx) # pylint: disable=no-member
|
||||
self.assertIn('results', resp.data) # pylint: disable=no-member
|
||||
self.assertEqual(len(resp.data['results']), num_ccx) # pylint: disable=no-member
|
||||
|
||||
def test_get_sorted_list(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_get_sorted_list(self, auth_attr):
|
||||
"""
|
||||
Tests the API to get a sorted list of CCX Courses
|
||||
"""
|
||||
@@ -312,21 +362,23 @@ class CcxListTest(CcxRestApiTest):
|
||||
|
||||
# sort by display name
|
||||
url = '{0}&order_by=display_name'.format(self.list_url_master_course)
|
||||
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(resp.data['results']), num_ccx) # pylint: disable=no-member
|
||||
# the display_name should be sorted as "Title CCX x", "Title CCX y", "Title CCX z"
|
||||
for num, ccx in enumerate(resp.data['results']): # pylint: disable=no-member
|
||||
self.assertEqual(title_str.format(string.ascii_lowercase[-(num_ccx - num)]), ccx['display_name'])
|
||||
|
||||
# add sort order desc
|
||||
url = '{0}&order_by=display_name&sort_order=desc'.format(self.list_url_master_course)
|
||||
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
# the only thing I can check is that the display name is in alphabetically reversed order
|
||||
# in the same way when the field has been updated above, so with the id asc
|
||||
for num, ccx in enumerate(resp.data['results']): # pylint: disable=no-member
|
||||
self.assertEqual(title_str.format(string.ascii_lowercase[-(num + 1)]), ccx['display_name'])
|
||||
|
||||
def test_get_paginated_list(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_get_paginated_list(self, auth_attr):
|
||||
"""
|
||||
Tests the API to get a paginated list of CCX Courses
|
||||
"""
|
||||
@@ -337,7 +389,7 @@ class CcxListTest(CcxRestApiTest):
|
||||
page_size = settings.REST_FRAMEWORK.get('PAGE_SIZE', 10)
|
||||
num_pages = int(math.ceil(num_ccx / float(page_size)))
|
||||
# get first page
|
||||
resp = self.client.get(self.list_url_master_course, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.get(self.list_url_master_course, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(resp.data['count'], num_ccx) # pylint: disable=no-member
|
||||
self.assertEqual(resp.data['num_pages'], num_pages) # pylint: disable=no-member
|
||||
@@ -345,9 +397,10 @@ class CcxListTest(CcxRestApiTest):
|
||||
self.assertEqual(resp.data['start'], 0) # pylint: disable=no-member
|
||||
self.assertIsNotNone(resp.data['next']) # pylint: disable=no-member
|
||||
self.assertIsNone(resp.data['previous']) # pylint: disable=no-member
|
||||
|
||||
# get a page in the middle
|
||||
url = '{0}&page=24'.format(self.list_url_master_course)
|
||||
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(resp.data['count'], num_ccx) # pylint: disable=no-member
|
||||
self.assertEqual(resp.data['num_pages'], num_pages) # pylint: disable=no-member
|
||||
@@ -355,9 +408,10 @@ class CcxListTest(CcxRestApiTest):
|
||||
self.assertEqual(resp.data['start'], (resp.data['current_page'] - 1) * page_size) # pylint: disable=no-member
|
||||
self.assertIsNotNone(resp.data['next']) # pylint: disable=no-member
|
||||
self.assertIsNotNone(resp.data['previous']) # pylint: disable=no-member
|
||||
|
||||
# get last page
|
||||
url = '{0}&page={1}'.format(self.list_url_master_course, num_pages)
|
||||
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(resp.data['count'], num_ccx) # pylint: disable=no-member
|
||||
self.assertEqual(resp.data['num_pages'], num_pages) # pylint: disable=no-member
|
||||
@@ -365,40 +419,76 @@ class CcxListTest(CcxRestApiTest):
|
||||
self.assertEqual(resp.data['start'], (resp.data['current_page'] - 1) * page_size) # pylint: disable=no-member
|
||||
self.assertIsNone(resp.data['next']) # pylint: disable=no-member
|
||||
self.assertIsNotNone(resp.data['previous']) # pylint: disable=no-member
|
||||
|
||||
# last page + 1
|
||||
url = '{0}&page={1}'.format(self.list_url_master_course, num_pages + 1)
|
||||
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@ddt.data(
|
||||
(
|
||||
{},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
'master_course_id_not_provided'
|
||||
'master_course_id_not_provided',
|
||||
'auth_header_oauth2_provider'
|
||||
),
|
||||
(
|
||||
{},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
'master_course_id_not_provided',
|
||||
'auth'
|
||||
),
|
||||
(
|
||||
{'master_course_id': None},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
'master_course_id_not_provided'
|
||||
'master_course_id_not_provided',
|
||||
'auth_header_oauth2_provider'
|
||||
),
|
||||
(
|
||||
{'master_course_id': None},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
'master_course_id_not_provided',
|
||||
'auth'
|
||||
),
|
||||
(
|
||||
{'master_course_id': ''},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
'course_id_not_valid'
|
||||
'course_id_not_valid',
|
||||
'auth_header_oauth2_provider'
|
||||
),
|
||||
(
|
||||
{'master_course_id': ''},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
'course_id_not_valid',
|
||||
'auth'
|
||||
),
|
||||
(
|
||||
{'master_course_id': 'invalid_master_course_str'},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
'course_id_not_valid'
|
||||
'course_id_not_valid',
|
||||
'auth'
|
||||
),
|
||||
(
|
||||
{'master_course_id': 'invalid_master_course_str'},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
'course_id_not_valid',
|
||||
'auth_header_oauth2_provider'
|
||||
),
|
||||
(
|
||||
{'master_course_id': 'course-v1:org_foo.0+course_bar_0+Run_0'},
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
'course_id_does_not_exist'
|
||||
'course_id_does_not_exist',
|
||||
'auth'
|
||||
),
|
||||
(
|
||||
{'master_course_id': 'course-v1:org_foo.0+course_bar_0+Run_0'},
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
'course_id_does_not_exist',
|
||||
'auth_header_oauth2_provider'
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_post_list_wrong_master_course(self, data, expected_http_error, expected_error_string):
|
||||
def test_post_list_wrong_master_course(self, data, expected_http_error, expected_error_string, auth_attr):
|
||||
"""
|
||||
Test for various post requests with wrong master course string
|
||||
"""
|
||||
@@ -407,10 +497,11 @@ class CcxListTest(CcxRestApiTest):
|
||||
with mock.patch(mock_class_str, autospec=True) as mocked_perm_class:
|
||||
mocked_perm_class.return_value = True
|
||||
# case with no master_course_id provided
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(expected_http_error, expected_error_string, resp)
|
||||
|
||||
def test_post_list_wrong_master_course_special_cases(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_post_list_wrong_master_course_special_cases(self, auth_attr):
|
||||
"""
|
||||
Same as test_post_list_wrong_master_course,
|
||||
but different ways to test the wrong master_course_id
|
||||
@@ -419,14 +510,17 @@ class CcxListTest(CcxRestApiTest):
|
||||
self.course.enable_ccx = False
|
||||
self.mstore.update_item(self.course, self.coach.id)
|
||||
data = {'master_course_id': self.master_course_key_str}
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_403_FORBIDDEN, 'ccx_not_enabled_for_master_course', resp)
|
||||
self.course.enable_ccx = True
|
||||
self.mstore.update_item(self.course, self.coach.id)
|
||||
|
||||
# case with deprecated master_course_id
|
||||
with mock.patch('courseware.courses.get_course_by_id', autospec=True) as mocked:
|
||||
mocked.return_value.id.deprecated = True
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
|
||||
self.expect_error(status.HTTP_400_BAD_REQUEST, 'deprecated_master_course_id', resp)
|
||||
|
||||
@ddt.data(
|
||||
@@ -436,7 +530,17 @@ class CcxListTest(CcxRestApiTest):
|
||||
'max_students_allowed': 'missing_field_max_students_allowed',
|
||||
'display_name': 'missing_field_display_name',
|
||||
'coach_email': 'missing_field_coach_email'
|
||||
}
|
||||
},
|
||||
'auth'
|
||||
),
|
||||
(
|
||||
{},
|
||||
{
|
||||
'max_students_allowed': 'missing_field_max_students_allowed',
|
||||
'display_name': 'missing_field_display_name',
|
||||
'coach_email': 'missing_field_coach_email'
|
||||
},
|
||||
'auth_header_oauth2_provider'
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -445,7 +549,18 @@ class CcxListTest(CcxRestApiTest):
|
||||
},
|
||||
{
|
||||
'coach_email': 'missing_field_coach_email'
|
||||
}
|
||||
},
|
||||
'auth'
|
||||
),
|
||||
(
|
||||
{
|
||||
'max_students_allowed': 10,
|
||||
'display_name': 'CCX Title'
|
||||
},
|
||||
{
|
||||
'coach_email': 'missing_field_coach_email'
|
||||
},
|
||||
'auth_header_oauth2_provider'
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -457,7 +572,21 @@ class CcxListTest(CcxRestApiTest):
|
||||
'max_students_allowed': 'null_field_max_students_allowed',
|
||||
'display_name': 'null_field_display_name',
|
||||
'coach_email': 'null_field_coach_email'
|
||||
}
|
||||
},
|
||||
'auth'
|
||||
),
|
||||
(
|
||||
{
|
||||
'max_students_allowed': None,
|
||||
'display_name': None,
|
||||
'coach_email': None
|
||||
},
|
||||
{
|
||||
'max_students_allowed': 'null_field_max_students_allowed',
|
||||
'display_name': 'null_field_display_name',
|
||||
'coach_email': 'null_field_coach_email'
|
||||
},
|
||||
'auth_header_oauth2_provider'
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -465,7 +594,17 @@ class CcxListTest(CcxRestApiTest):
|
||||
'display_name': 'CCX Title',
|
||||
'coach_email': 'this is not an email@test.com'
|
||||
},
|
||||
{'coach_email': 'invalid_coach_email'}
|
||||
{'coach_email': 'invalid_coach_email'},
|
||||
'auth'
|
||||
),
|
||||
(
|
||||
{
|
||||
'max_students_allowed': 10,
|
||||
'display_name': 'CCX Title',
|
||||
'coach_email': 'this is not an email@test.com'
|
||||
},
|
||||
{'coach_email': 'invalid_coach_email'},
|
||||
'auth_header_oauth2_provider'
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -473,7 +612,17 @@ class CcxListTest(CcxRestApiTest):
|
||||
'display_name': '',
|
||||
'coach_email': 'email@test.com'
|
||||
},
|
||||
{'display_name': 'invalid_display_name'}
|
||||
{'display_name': 'invalid_display_name'},
|
||||
'auth'
|
||||
),
|
||||
(
|
||||
{
|
||||
'max_students_allowed': 10,
|
||||
'display_name': '',
|
||||
'coach_email': 'email@test.com'
|
||||
},
|
||||
{'display_name': 'invalid_display_name'},
|
||||
'auth_header_oauth2_provider'
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -481,7 +630,17 @@ class CcxListTest(CcxRestApiTest):
|
||||
'display_name': 'CCX Title',
|
||||
'coach_email': 'email@test.com'
|
||||
},
|
||||
{'max_students_allowed': 'invalid_max_students_allowed'}
|
||||
{'max_students_allowed': 'invalid_max_students_allowed'},
|
||||
'auth'
|
||||
),
|
||||
(
|
||||
{
|
||||
'max_students_allowed': 'a',
|
||||
'display_name': 'CCX Title',
|
||||
'coach_email': 'email@test.com'
|
||||
},
|
||||
{'max_students_allowed': 'invalid_max_students_allowed'},
|
||||
'auth_header_oauth2_provider'
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -490,7 +649,18 @@ class CcxListTest(CcxRestApiTest):
|
||||
'coach_email': 'email@test.com',
|
||||
'course_modules': {'foo': 'bar'}
|
||||
},
|
||||
{'course_modules': 'invalid_course_module_list'}
|
||||
{'course_modules': 'invalid_course_module_list'},
|
||||
'auth'
|
||||
),
|
||||
(
|
||||
{
|
||||
'max_students_allowed': 10,
|
||||
'display_name': 'CCX Title',
|
||||
'coach_email': 'email@test.com',
|
||||
'course_modules': {'foo': 'bar'}
|
||||
},
|
||||
{'course_modules': 'invalid_course_module_list'},
|
||||
'auth_header_oauth2_provider'
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -499,7 +669,18 @@ class CcxListTest(CcxRestApiTest):
|
||||
'coach_email': 'email@test.com',
|
||||
'course_modules': 'block-v1:org.0+course_0+Run_0+type@chapter+block@chapter_1'
|
||||
},
|
||||
{'course_modules': 'invalid_course_module_list'}
|
||||
{'course_modules': 'invalid_course_module_list'},
|
||||
'auth'
|
||||
),
|
||||
(
|
||||
{
|
||||
'max_students_allowed': 10,
|
||||
'display_name': 'CCX Title',
|
||||
'coach_email': 'email@test.com',
|
||||
'course_modules': 'block-v1:org.0+course_0+Run_0+type@chapter+block@chapter_1'
|
||||
},
|
||||
{'course_modules': 'invalid_course_module_list'},
|
||||
'auth_header_oauth2_provider'
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -508,20 +689,32 @@ class CcxListTest(CcxRestApiTest):
|
||||
'coach_email': 'email@test.com',
|
||||
'course_modules': ['foo', 'bar']
|
||||
},
|
||||
{'course_modules': 'invalid_course_module_keys'}
|
||||
{'course_modules': 'invalid_course_module_keys'},
|
||||
'auth'
|
||||
),
|
||||
(
|
||||
{
|
||||
'max_students_allowed': 10,
|
||||
'display_name': 'CCX Title',
|
||||
'coach_email': 'email@test.com',
|
||||
'course_modules': ['foo', 'bar']
|
||||
},
|
||||
{'course_modules': 'invalid_course_module_keys'},
|
||||
'auth_header_oauth2_provider'
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_post_list_wrong_input_data(self, data, expected_errors):
|
||||
def test_post_list_wrong_input_data(self, data, expected_errors, auth_attr):
|
||||
"""
|
||||
Test for various post requests with wrong input data
|
||||
"""
|
||||
# add the master_course_key_str to the request data
|
||||
data['master_course_id'] = self.master_course_key_str
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error_fields(expected_errors, resp)
|
||||
|
||||
def test_post_list_coach_does_not_exist(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_post_list_coach_does_not_exist(self, auth_attr):
|
||||
"""
|
||||
Specific test for the case when the input data is valid but the coach does not exist.
|
||||
"""
|
||||
@@ -531,10 +724,11 @@ class CcxListTest(CcxRestApiTest):
|
||||
'display_name': 'CCX Title',
|
||||
'coach_email': 'inexisting_email@test.com'
|
||||
}
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_404_NOT_FOUND, 'coach_user_does_not_exist', resp)
|
||||
|
||||
def test_post_list_wrong_modules(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_post_list_wrong_modules(self, auth_attr):
|
||||
"""
|
||||
Specific test for the case when the input data is valid but the
|
||||
course modules do not belong to the master course
|
||||
@@ -549,10 +743,11 @@ class CcxListTest(CcxRestApiTest):
|
||||
'block-v1:org.0+course_0+Run_0+type@chapter+block@chapter_bar'
|
||||
]
|
||||
}
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_400_BAD_REQUEST, 'course_module_list_not_belonging_to_master_course', resp)
|
||||
|
||||
def test_post_list_mixed_wrong_and_valid_modules(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_post_list_mixed_wrong_and_valid_modules(self, auth_attr):
|
||||
"""
|
||||
Specific test for the case when the input data is valid but some of
|
||||
the course modules do not belong to the master course
|
||||
@@ -565,10 +760,11 @@ class CcxListTest(CcxRestApiTest):
|
||||
'coach_email': self.coach.email,
|
||||
'course_modules': modules
|
||||
}
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_400_BAD_REQUEST, 'course_module_list_not_belonging_to_master_course', resp)
|
||||
|
||||
def test_post_list(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_post_list(self, auth_attr):
|
||||
"""
|
||||
Test the creation of a CCX
|
||||
"""
|
||||
@@ -580,7 +776,7 @@ class CcxListTest(CcxRestApiTest):
|
||||
'coach_email': self.coach.email,
|
||||
'course_modules': self.master_course_chapters[0:1]
|
||||
}
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
|
||||
# check if the response has at least the same data of the request
|
||||
for key, val in data.iteritems():
|
||||
@@ -605,7 +801,36 @@ class CcxListTest(CcxRestApiTest):
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertIn(self.coach.email, outbox[0].recipients()) # pylint: disable=no-member
|
||||
|
||||
def test_post_list_duplicated_modules(self):
|
||||
@ddt.data(
|
||||
('auth', True),
|
||||
('auth', False),
|
||||
('auth_header_oauth2_provider', True),
|
||||
('auth_header_oauth2_provider', False)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_post_list_on_active_state(self, auth_attr, user_is_active):
|
||||
"""
|
||||
Test the creation of a CCX on user's active states.
|
||||
"""
|
||||
self.app_user.is_active = user_is_active
|
||||
self.app_user.save() # pylint: disable=no-member
|
||||
|
||||
data = {
|
||||
'master_course_id': self.master_course_key_str,
|
||||
'max_students_allowed': 111,
|
||||
'display_name': 'CCX Test Title',
|
||||
'coach_email': self.coach.email,
|
||||
'course_modules': self.master_course_chapters[0:1]
|
||||
}
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
|
||||
if not user_is_active:
|
||||
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||
else:
|
||||
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_post_list_duplicated_modules(self, auth_attr):
|
||||
"""
|
||||
Test the creation of a CCX, but with duplicated modules
|
||||
"""
|
||||
@@ -618,11 +843,12 @@ class CcxListTest(CcxRestApiTest):
|
||||
'coach_email': self.coach.email,
|
||||
'course_modules': duplicated_chapters
|
||||
}
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(resp.data.get('course_modules'), chapters) # pylint: disable=no-member
|
||||
|
||||
def test_post_list_staff_master_course_in_ccx(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_post_list_staff_master_course_in_ccx(self, auth_attr):
|
||||
"""
|
||||
Specific test to check that the staff and instructor of the master
|
||||
course are assigned to the CCX.
|
||||
@@ -634,7 +860,7 @@ class CcxListTest(CcxRestApiTest):
|
||||
'display_name': 'CCX Test Title',
|
||||
'coach_email': self.coach.email
|
||||
}
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
|
||||
# check that only one email has been sent and it is to to the coach
|
||||
self.assertEqual(len(outbox), 1)
|
||||
@@ -687,7 +913,7 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
ccx.structure_json = json.dumps(self.master_course_chapters)
|
||||
ccx.save()
|
||||
|
||||
today = datetime.datetime.today()
|
||||
today = datetime.today()
|
||||
start = today.replace(tzinfo=pytz.UTC)
|
||||
override_field_for_ccx(ccx, self.course, 'start', start)
|
||||
override_field_for_ccx(ccx, self.course, 'due', None)
|
||||
@@ -716,7 +942,8 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
)
|
||||
return ccx
|
||||
|
||||
def test_authorization(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_authorization(self, auth_attr):
|
||||
"""
|
||||
Test that only the right token is authorized
|
||||
"""
|
||||
@@ -731,7 +958,8 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
for auth in auth_list:
|
||||
resp = self.client.get(self.detail_url, {}, HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
resp = self.client.get(self.detail_url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
|
||||
resp = self.client.get(self.detail_url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_authorization_no_oauth_staff(self):
|
||||
@@ -745,7 +973,7 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
|
||||
data = {'display_name': 'CCX Title'}
|
||||
# the staff user can perform the request
|
||||
self.client.login(username=staff_user.username, password='test')
|
||||
self.client.login(username=staff_user.username, password=USER_PASSWORD)
|
||||
resp = self.client.get(self.detail_url)
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
resp = self.client.patch(self.detail_url, data, format='json')
|
||||
@@ -762,7 +990,7 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
|
||||
data = {'display_name': 'CCX Title'}
|
||||
# the instructor user can perform the request
|
||||
self.client.login(username=instructor_user.username, password='test')
|
||||
self.client.login(username=instructor_user.username, password=USER_PASSWORD)
|
||||
resp = self.client.get(self.detail_url)
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
resp = self.client.patch(self.detail_url, data, format='json')
|
||||
@@ -779,7 +1007,7 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
|
||||
data = {'display_name': 'CCX Title'}
|
||||
# the coach user cannot perform the request: this type of user can only get her own CCX
|
||||
self.client.login(username=coach_user.username, password='test')
|
||||
self.client.login(username=coach_user.username, password=USER_PASSWORD)
|
||||
resp = self.client.get(self.detail_url)
|
||||
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||
resp = self.client.patch(self.detail_url, data, format='json')
|
||||
@@ -791,7 +1019,7 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
"""
|
||||
data = {'display_name': 'CCX Title'}
|
||||
# the coach owner of the CCX can perform the request only if it is a get
|
||||
self.client.login(username=self.coach.username, password='test')
|
||||
self.client.login(username=self.coach.username, password=USER_PASSWORD)
|
||||
resp = self.client.get(self.detail_url)
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
resp = self.client.patch(self.detail_url, data, format='json')
|
||||
@@ -821,9 +1049,16 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
self.assertEqual(views.CCXDetailView.__name__, resolver.func.__name__)
|
||||
self.assertEqual(views.CCXDetailView.__module__, resolver.func.__module__)
|
||||
|
||||
@ddt.data(('get',), ('delete',), ('patch',))
|
||||
@ddt.data(
|
||||
('get', AUTH_ATTRS[0]),
|
||||
('get', AUTH_ATTRS[1]),
|
||||
('delete', AUTH_ATTRS[0]),
|
||||
('delete', AUTH_ATTRS[1]),
|
||||
('patch', AUTH_ATTRS[0]),
|
||||
('patch', AUTH_ATTRS[1])
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_detail_wrong_ccx(self, http_method):
|
||||
def test_detail_wrong_ccx(self, http_method, auth_attr):
|
||||
"""
|
||||
Test for different methods for detail of a ccx course.
|
||||
All check the validity of the ccx course id
|
||||
@@ -834,40 +1069,46 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
url = reverse('ccx_api:v0:ccx:detail', kwargs={'ccx_course_id': self.master_course_key_str})
|
||||
|
||||
# the permission class will give a 403 error because will not find the CCX
|
||||
resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = client_request(url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# bypassing the permission class we get another kind of error
|
||||
with mock.patch(mock_class_str, autospec=True) as mocked_perm_class:
|
||||
mocked_perm_class.return_value = True
|
||||
resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = client_request(url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_400_BAD_REQUEST, 'course_id_not_valid_ccx_id', resp)
|
||||
|
||||
# use an non existing ccx id
|
||||
url = reverse('ccx_api:v0:ccx:detail', kwargs={'ccx_course_id': 'ccx-v1:foo.0+course_bar_0+Run_0+ccx@1'})
|
||||
# the permission class will give a 403 error because will not find the CCX
|
||||
resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = client_request(url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# bypassing the permission class we get another kind of error
|
||||
with mock.patch(mock_class_str, autospec=True) as mocked_perm_class:
|
||||
mocked_perm_class.return_value = True
|
||||
resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = client_request(url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_404_NOT_FOUND, 'ccx_course_id_does_not_exist', resp)
|
||||
|
||||
# get a valid ccx key and add few 0s to get a non existing ccx for a valid course
|
||||
ccx_key_str = '{0}000000'.format(self.ccx_key_str)
|
||||
url = reverse('ccx_api:v0:ccx:detail', kwargs={'ccx_course_id': ccx_key_str})
|
||||
# the permission class will give a 403 error because will not find the CCX
|
||||
resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = client_request(url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# bypassing the permission class we get another kind of error
|
||||
with mock.patch(mock_class_str, autospec=True) as mocked_perm_class:
|
||||
mocked_perm_class.return_value = True
|
||||
resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = client_request(url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_404_NOT_FOUND, 'ccx_course_id_does_not_exist', resp)
|
||||
|
||||
def test_get_detail(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_get_detail(self, auth_attr):
|
||||
"""
|
||||
Test for getting detail of a ccx course
|
||||
"""
|
||||
resp = self.client.get(self.detail_url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.get(self.detail_url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(resp.data.get('ccx_course_id'), self.ccx_key_str) # pylint: disable=no-member
|
||||
self.assertEqual(resp.data.get('display_name'), self.ccx.display_name) # pylint: disable=no-member
|
||||
@@ -879,14 +1120,15 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
self.assertEqual(resp.data.get('master_course_id'), unicode(self.ccx.course_id)) # pylint: disable=no-member
|
||||
self.assertItemsEqual(resp.data.get('course_modules'), self.master_course_chapters) # pylint: disable=no-member
|
||||
|
||||
def test_delete_detail(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_delete_detail(self, auth_attr):
|
||||
"""
|
||||
Test for deleting a ccx course
|
||||
"""
|
||||
# check that there are overrides
|
||||
self.assertGreater(CcxFieldOverride.objects.filter(ccx=self.ccx).count(), 0)
|
||||
self.assertGreater(CourseEnrollment.objects.filter(course_id=self.ccx_key).count(), 0)
|
||||
resp = self.client.delete(self.detail_url, {}, HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.delete(self.detail_url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||
self.assertIsNone(resp.data) # pylint: disable=no-member
|
||||
# the CCX does not exist any more
|
||||
@@ -896,14 +1138,15 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
self.assertEqual(CcxFieldOverride.objects.filter(ccx=self.ccx).count(), 0)
|
||||
self.assertEqual(CourseEnrollment.objects.filter(course_id=self.ccx_key).count(), 0)
|
||||
|
||||
def test_patch_detail_change_master_course(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_patch_detail_change_master_course(self, auth_attr):
|
||||
"""
|
||||
Test to patch a ccx course to change a master course
|
||||
"""
|
||||
data = {
|
||||
'master_course_id': 'changed_course_id'
|
||||
}
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_403_FORBIDDEN, 'master_course_id_change_not_allowed', resp)
|
||||
|
||||
@ddt.data(
|
||||
@@ -917,42 +1160,95 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
'max_students_allowed': 'null_field_max_students_allowed',
|
||||
'display_name': 'null_field_display_name',
|
||||
'coach_email': 'null_field_coach_email'
|
||||
}
|
||||
},
|
||||
AUTH_ATTRS[0]
|
||||
),
|
||||
(
|
||||
{
|
||||
'max_students_allowed': None,
|
||||
'display_name': None,
|
||||
'coach_email': None
|
||||
},
|
||||
{
|
||||
'max_students_allowed': 'null_field_max_students_allowed',
|
||||
'display_name': 'null_field_display_name',
|
||||
'coach_email': 'null_field_coach_email'
|
||||
},
|
||||
AUTH_ATTRS[1]
|
||||
),
|
||||
(
|
||||
{'coach_email': 'this is not an email@test.com'},
|
||||
{'coach_email': 'invalid_coach_email'}
|
||||
{'coach_email': 'invalid_coach_email'},
|
||||
AUTH_ATTRS[0]
|
||||
),
|
||||
(
|
||||
{'coach_email': 'this is not an email@test.com'},
|
||||
{'coach_email': 'invalid_coach_email'},
|
||||
AUTH_ATTRS[1]
|
||||
),
|
||||
(
|
||||
{'display_name': ''},
|
||||
{'display_name': 'invalid_display_name'}
|
||||
{'display_name': 'invalid_display_name'},
|
||||
AUTH_ATTRS[0]
|
||||
),
|
||||
(
|
||||
{'display_name': ''},
|
||||
{'display_name': 'invalid_display_name'},
|
||||
AUTH_ATTRS[1]
|
||||
),
|
||||
(
|
||||
{'max_students_allowed': 'a'},
|
||||
{'max_students_allowed': 'invalid_max_students_allowed'}
|
||||
{'max_students_allowed': 'invalid_max_students_allowed'},
|
||||
AUTH_ATTRS[0]
|
||||
),
|
||||
(
|
||||
{'max_students_allowed': 'a'},
|
||||
{'max_students_allowed': 'invalid_max_students_allowed'},
|
||||
AUTH_ATTRS[1]
|
||||
),
|
||||
(
|
||||
{'course_modules': {'foo': 'bar'}},
|
||||
{'course_modules': 'invalid_course_module_list'}
|
||||
{'course_modules': 'invalid_course_module_list'},
|
||||
AUTH_ATTRS[0]
|
||||
),
|
||||
(
|
||||
{'course_modules': {'foo': 'bar'}},
|
||||
{'course_modules': 'invalid_course_module_list'},
|
||||
AUTH_ATTRS[1]
|
||||
),
|
||||
(
|
||||
{'course_modules': 'block-v1:org.0+course_0+Run_0+type@chapter+block@chapter_1'},
|
||||
{'course_modules': 'invalid_course_module_list'}
|
||||
{'course_modules': 'invalid_course_module_list'},
|
||||
AUTH_ATTRS[0]
|
||||
|
||||
),
|
||||
(
|
||||
{'course_modules': 'block-v1:org.0+course_0+Run_0+type@chapter+block@chapter_1'},
|
||||
{'course_modules': 'invalid_course_module_list'},
|
||||
AUTH_ATTRS[1]
|
||||
|
||||
),
|
||||
(
|
||||
{'course_modules': ['foo', 'bar']},
|
||||
{'course_modules': 'invalid_course_module_keys'}
|
||||
{'course_modules': 'invalid_course_module_keys'},
|
||||
AUTH_ATTRS[0]
|
||||
),
|
||||
(
|
||||
{'course_modules': ['foo', 'bar']},
|
||||
{'course_modules': 'invalid_course_module_keys'},
|
||||
AUTH_ATTRS[1]
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_patch_detail_wrong_input_data(self, data, expected_errors):
|
||||
def test_patch_detail_wrong_input_data(self, data, expected_errors, auth_attr):
|
||||
"""
|
||||
Test for different wrong inputs for the patch method
|
||||
"""
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error_fields(expected_errors, resp)
|
||||
|
||||
def test_empty_patch(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_empty_patch(self, auth_attr):
|
||||
"""
|
||||
An empty patch does not modify anything
|
||||
"""
|
||||
@@ -960,7 +1256,7 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
max_students_allowed = self.ccx.max_student_enrollments_allowed # pylint: disable=no-member
|
||||
coach_email = self.ccx.coach.email # pylint: disable=no-member
|
||||
ccx_structure = self.ccx.structure # pylint: disable=no-member
|
||||
resp = self.client.patch(self.detail_url, {}, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.patch(self.detail_url, {}, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||
ccx = CustomCourseForEdX.objects.get(id=self.ccx.id)
|
||||
self.assertEqual(display_name, ccx.display_name)
|
||||
@@ -968,7 +1264,8 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
self.assertEqual(coach_email, ccx.coach.email)
|
||||
self.assertEqual(ccx_structure, ccx.structure)
|
||||
|
||||
def test_patch_detail_coach_does_not_exist(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_patch_detail_coach_does_not_exist(self, auth_attr):
|
||||
"""
|
||||
Specific test for the case when the input data is valid but the coach does not exist.
|
||||
"""
|
||||
@@ -977,10 +1274,11 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
'display_name': 'CCX Title',
|
||||
'coach_email': 'inexisting_email@test.com'
|
||||
}
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_404_NOT_FOUND, 'coach_user_does_not_exist', resp)
|
||||
|
||||
def test_patch_detail_wrong_modules(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_patch_detail_wrong_modules(self, auth_attr):
|
||||
"""
|
||||
Specific test for the case when the input data is valid but the
|
||||
course modules do not belong to the master course
|
||||
@@ -991,10 +1289,11 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
'block-v1:org.0+course_0+Run_0+type@chapter+block@chapter_bar'
|
||||
]
|
||||
}
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_400_BAD_REQUEST, 'course_module_list_not_belonging_to_master_course', resp)
|
||||
|
||||
def test_patch_detail_mixed_wrong_and_valid_modules(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_patch_detail_mixed_wrong_and_valid_modules(self, auth_attr):
|
||||
"""
|
||||
Specific test for the case when the input data is valid but some of
|
||||
the course modules do not belong to the master course
|
||||
@@ -1003,10 +1302,11 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
data = {
|
||||
'course_modules': modules
|
||||
}
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.expect_error(status.HTTP_400_BAD_REQUEST, 'course_module_list_not_belonging_to_master_course', resp)
|
||||
|
||||
def test_patch_detail(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_patch_detail(self, auth_attr):
|
||||
"""
|
||||
Test for successful patch
|
||||
"""
|
||||
@@ -1018,7 +1318,7 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
'display_name': 'CCX Title',
|
||||
'coach_email': new_coach.email
|
||||
}
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||
ccx_from_db = CustomCourseForEdX.objects.get(id=self.ccx.id)
|
||||
self.assertEqual(ccx_from_db.max_student_enrollments_allowed, data['max_students_allowed'])
|
||||
@@ -1036,37 +1336,93 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertIn(new_coach.email, outbox[0].recipients()) # pylint: disable=no-member
|
||||
|
||||
def test_patch_detail_modules(self):
|
||||
@ddt.data(*AUTH_ATTRS)
|
||||
def test_patch_detail_modules(self, auth_attr):
|
||||
"""
|
||||
Specific test for successful patch of the course modules
|
||||
"""
|
||||
data = {'course_modules': self.master_course_chapters[0:1]}
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||
ccx_from_db = CustomCourseForEdX.objects.get(id=self.ccx.id)
|
||||
self.assertItemsEqual(ccx_from_db.structure, data['course_modules'])
|
||||
|
||||
data = {'course_modules': []}
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||
ccx_from_db = CustomCourseForEdX.objects.get(id=self.ccx.id)
|
||||
self.assertItemsEqual(ccx_from_db.structure, [])
|
||||
|
||||
data = {'course_modules': self.master_course_chapters}
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||
ccx_from_db = CustomCourseForEdX.objects.get(id=self.ccx.id)
|
||||
self.assertItemsEqual(ccx_from_db.structure, self.master_course_chapters)
|
||||
|
||||
data = {'course_modules': None}
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||
ccx_from_db = CustomCourseForEdX.objects.get(id=self.ccx.id)
|
||||
self.assertEqual(ccx_from_db.structure, None)
|
||||
|
||||
chapters = self.master_course_chapters[0:1]
|
||||
data = {'course_modules': chapters * 3}
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||
ccx_from_db = CustomCourseForEdX.objects.get(id=self.ccx.id)
|
||||
self.assertItemsEqual(ccx_from_db.structure, chapters)
|
||||
|
||||
@ddt.data(
|
||||
('auth', True),
|
||||
('auth', False),
|
||||
('auth_header_oauth2_provider', True),
|
||||
('auth_header_oauth2_provider', False)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_patch_user_on_active_state(self, auth_attr, user_is_active):
|
||||
"""
|
||||
Test patch ccx course on user's active state.
|
||||
"""
|
||||
self.app_user.is_active = user_is_active
|
||||
self.app_user.save() # pylint: disable=no-member
|
||||
|
||||
chapters = self.master_course_chapters[0:1]
|
||||
data = {'course_modules': chapters * 3}
|
||||
resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
if not user_is_active:
|
||||
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||
else:
|
||||
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||
ccx_from_db = CustomCourseForEdX.objects.get(id=self.ccx.id)
|
||||
self.assertItemsEqual(ccx_from_db.structure, chapters)
|
||||
|
||||
@ddt.data(
|
||||
('auth', True),
|
||||
('auth', False),
|
||||
('auth_header_oauth2_provider', True),
|
||||
('auth_header_oauth2_provider', False)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_delete_detail_on_active_state(self, auth_attr, user_is_active):
|
||||
"""
|
||||
Test for deleting a ccx course on user's active state.
|
||||
"""
|
||||
self.app_user.is_active = user_is_active
|
||||
self.app_user.save() # pylint: disable=no-member
|
||||
|
||||
# check that there are overrides
|
||||
self.assertGreater(CcxFieldOverride.objects.filter(ccx=self.ccx).count(), 0)
|
||||
self.assertGreater(CourseEnrollment.objects.filter(course_id=self.ccx_key).count(), 0)
|
||||
resp = self.client.delete(self.detail_url, {}, HTTP_AUTHORIZATION=getattr(self, auth_attr))
|
||||
|
||||
if not user_is_active:
|
||||
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||
else:
|
||||
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||
self.assertIsNone(resp.data) # pylint: disable=no-member
|
||||
# the CCX does not exist any more
|
||||
with self.assertRaises(CustomCourseForEdX.DoesNotExist):
|
||||
CustomCourseForEdX.objects.get(id=self.ccx.id)
|
||||
# check that there are no overrides
|
||||
self.assertEqual(CcxFieldOverride.objects.filter(ccx=self.ccx).count(), 0)
|
||||
self.assertEqual(CourseEnrollment.objects.filter(course_id=self.ccx_key).count(), 0)
|
||||
|
||||
@@ -9,11 +9,9 @@ from django.contrib.auth.models import User
|
||||
from django.db import transaction
|
||||
from django.http import Http404
|
||||
from rest_framework import status
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_oauth.authentication import OAuth2Authentication
|
||||
|
||||
from ccx_keys.locator import CCXLocator
|
||||
from courseware import courses
|
||||
@@ -26,7 +24,10 @@ from lms.djangoapps.instructor.enrollment import (
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.lib.api import permissions
|
||||
from openedx.core.lib.api import (
|
||||
authentication,
|
||||
permissions,
|
||||
)
|
||||
from student.models import CourseEnrollment
|
||||
from student.roles import CourseCcxCoachRole
|
||||
|
||||
@@ -363,7 +364,11 @@ class CCXListView(GenericAPIView):
|
||||
]
|
||||
}
|
||||
"""
|
||||
authentication_classes = (JwtAuthentication, OAuth2Authentication, SessionAuthentication,)
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
authentication.OAuth2AuthenticationAllowInactiveUser,
|
||||
authentication.SessionAuthenticationAllowInactiveUser,
|
||||
)
|
||||
permission_classes = (IsAuthenticated, permissions.IsMasterCourseStaffInstructor)
|
||||
serializer_class = CCXCourseSerializer
|
||||
pagination_class = CCXAPIPagination
|
||||
@@ -609,7 +614,11 @@ class CCXDetailView(GenericAPIView):
|
||||
response is returned.
|
||||
"""
|
||||
|
||||
authentication_classes = (JwtAuthentication, OAuth2Authentication, SessionAuthentication,)
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
authentication.OAuth2AuthenticationAllowInactiveUser,
|
||||
authentication.SessionAuthenticationAllowInactiveUser,
|
||||
)
|
||||
permission_classes = (IsAuthenticated, permissions.IsCourseStaffInstructor)
|
||||
serializer_class = CCXCourseSerializer
|
||||
|
||||
|
||||
Reference in New Issue
Block a user