This change is a follow-up to the chages in PR 8347, which removed the edX login page from the workflow for a new user. Where previously we redirected a user to the login page, PR 8347 instead creates a new user transparently and logs them in. The initial reason for splitting the LTI view between lti_launch and lti_run was so that there was a target for the GET request that followed the login page. Since we no longer use the login page, we no longer need the second view. We also don't need to store the LTI parameters in the session any more, since they are not persisting between calls. This simplifies the view logic significantly. The other change here is to fetch the LtiConsumer object early in the view, and pass it to the SignatureValidator and scoring system. When the views were split, this required multiple DB hits for the same data; we're now only fetching it once.
189 lines
6.8 KiB
Python
189 lines
6.8 KiB
Python
"""
|
|
Tests for the LTI provider views
|
|
"""
|
|
|
|
from django.core.urlresolvers import reverse
|
|
from django.test import TestCase
|
|
from django.test.client import RequestFactory
|
|
from mock import patch, MagicMock
|
|
|
|
from courseware.testutils import RenderXBlockTestMixin
|
|
from lti_provider import views, models
|
|
from lti_provider.signature_validator import SignatureValidator
|
|
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
|
|
from student.tests.factories import UserFactory
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
|
|
|
|
|
LTI_DEFAULT_PARAMS = {
|
|
'roles': u'Instructor,urn:lti:instrole:ims/lis/Administrator',
|
|
'context_id': u'lti_launch_context_id',
|
|
'oauth_version': u'1.0',
|
|
'oauth_consumer_key': u'consumer_key',
|
|
'oauth_signature': u'OAuth Signature',
|
|
'oauth_signature_method': u'HMAC-SHA1',
|
|
'oauth_timestamp': u'OAuth Timestamp',
|
|
'oauth_nonce': u'OAuth Nonce',
|
|
'user_id': u'LTI_User',
|
|
}
|
|
|
|
LTI_OPTIONAL_PARAMS = {
|
|
'lis_result_sourcedid': u'result sourcedid',
|
|
'lis_outcome_service_url': u'outcome service URL',
|
|
'tool_consumer_instance_guid': u'consumer instance guid'
|
|
}
|
|
|
|
COURSE_KEY = CourseLocator(org='some_org', course='some_course', run='some_run')
|
|
USAGE_KEY = BlockUsageLocator(course_key=COURSE_KEY, block_type='problem', block_id='block_id')
|
|
|
|
COURSE_PARAMS = {
|
|
'course_key': COURSE_KEY,
|
|
'usage_key': USAGE_KEY
|
|
}
|
|
|
|
|
|
ALL_PARAMS = dict(LTI_DEFAULT_PARAMS.items() + COURSE_PARAMS.items())
|
|
|
|
|
|
def build_launch_request(authenticated=True):
|
|
"""
|
|
Helper method to create a new request object for the LTI launch.
|
|
"""
|
|
request = RequestFactory().post('/')
|
|
request.user = UserFactory.create()
|
|
request.user.is_authenticated = MagicMock(return_value=authenticated)
|
|
request.session = {}
|
|
request.POST.update(LTI_DEFAULT_PARAMS)
|
|
return request
|
|
|
|
|
|
class LtiTestMixin(object):
|
|
"""
|
|
Mixin for LTI tests
|
|
"""
|
|
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_LTI_PROVIDER': True})
|
|
def setUp(self):
|
|
super(LtiTestMixin, self).setUp()
|
|
# Always accept the OAuth signature
|
|
SignatureValidator.verify = MagicMock(return_value=True)
|
|
self.consumer = models.LtiConsumer(
|
|
consumer_name='consumer',
|
|
consumer_key=LTI_DEFAULT_PARAMS['oauth_consumer_key'],
|
|
consumer_secret='secret'
|
|
)
|
|
self.consumer.save()
|
|
|
|
|
|
class LtiLaunchTest(LtiTestMixin, TestCase):
|
|
"""
|
|
Tests for the lti_launch view
|
|
"""
|
|
@patch('lti_provider.views.render_courseware')
|
|
@patch('lti_provider.views.authenticate_lti_user')
|
|
def test_valid_launch(self, _authenticate, render):
|
|
"""
|
|
Verifies that the LTI launch succeeds when passed a valid request.
|
|
"""
|
|
request = build_launch_request()
|
|
views.lti_launch(request, unicode(COURSE_KEY), unicode(USAGE_KEY))
|
|
render.assert_called_with(request, USAGE_KEY)
|
|
|
|
@patch('lti_provider.views.render_courseware')
|
|
@patch('lti_provider.views.store_outcome_parameters')
|
|
@patch('lti_provider.views.authenticate_lti_user')
|
|
def test_outcome_service_registered(self, _authenticate, store_params, _render):
|
|
"""
|
|
Verifies that the LTI launch succeeds when passed a valid request.
|
|
"""
|
|
request = build_launch_request()
|
|
views.lti_launch(
|
|
request,
|
|
unicode(COURSE_PARAMS['course_key']),
|
|
unicode(COURSE_PARAMS['usage_key'])
|
|
)
|
|
store_params.assert_called_with(ALL_PARAMS, request.user, self.consumer)
|
|
|
|
def launch_with_missing_parameter(self, missing_param):
|
|
"""
|
|
Helper method to remove a parameter from the LTI launch and call the view
|
|
"""
|
|
request = build_launch_request()
|
|
del request.POST[missing_param]
|
|
return views.lti_launch(request, None, None)
|
|
|
|
def test_launch_with_missing_parameters(self):
|
|
"""
|
|
Runs through all required LTI parameters and verifies that the lti_launch
|
|
view returns Bad Request if any of them are missing.
|
|
"""
|
|
for missing_param in views.REQUIRED_PARAMETERS:
|
|
response = self.launch_with_missing_parameter(missing_param)
|
|
self.assertEqual(
|
|
response.status_code, 400,
|
|
'Launch should fail when parameter ' + missing_param + ' is missing'
|
|
)
|
|
|
|
def test_launch_with_disabled_feature_flag(self):
|
|
"""
|
|
Verifies that the LTI launch will fail if the ENABLE_LTI_PROVIDER flag
|
|
is not set
|
|
"""
|
|
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_LTI_PROVIDER': False}):
|
|
request = build_launch_request()
|
|
response = views.lti_launch(request, None, None)
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
def test_forbidden_if_signature_fails(self):
|
|
"""
|
|
Verifies that the view returns Forbidden if the LTI OAuth signature is
|
|
incorrect.
|
|
"""
|
|
SignatureValidator.verify = MagicMock(return_value=False)
|
|
request = build_launch_request()
|
|
response = views.lti_launch(request, None, None)
|
|
self.assertEqual(response.status_code, 403)
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
@patch('lti_provider.views.render_courseware')
|
|
def test_lti_consumer_record_supplemented_with_guid(self, _render):
|
|
SignatureValidator.verify = MagicMock(return_value=False)
|
|
request = build_launch_request()
|
|
request.POST.update(LTI_OPTIONAL_PARAMS)
|
|
with self.assertNumQueries(4):
|
|
views.lti_launch(request, None, None)
|
|
consumer = models.LtiConsumer.objects.get(
|
|
consumer_key=LTI_DEFAULT_PARAMS['oauth_consumer_key']
|
|
)
|
|
self.assertEqual(consumer.instance_guid, u'consumer instance guid')
|
|
|
|
|
|
class LtiLaunchTestRender(LtiTestMixin, RenderXBlockTestMixin, ModuleStoreTestCase):
|
|
"""
|
|
Tests for the rendering returned by lti_launch view.
|
|
This class overrides the get_response method, which is used by
|
|
the tests defined in RenderXBlockTestMixin.
|
|
"""
|
|
def get_response(self):
|
|
"""
|
|
Overridable method to get the response from the endpoint that is being tested.
|
|
"""
|
|
lti_launch_url = reverse(
|
|
'lti_provider_launch',
|
|
kwargs={
|
|
'course_id': unicode(self.course.id),
|
|
'usage_id': unicode(self.html_block.location)
|
|
}
|
|
)
|
|
SignatureValidator.verify = MagicMock(return_value=True)
|
|
return self.client.post(lti_launch_url, data=LTI_DEFAULT_PARAMS)
|
|
|
|
def test_unenrolled_student(self):
|
|
self.setup_course()
|
|
self.setup_user(admin=False, enroll=False, login=True)
|
|
self.verify_response()
|
|
|
|
def test_unauthenticated(self):
|
|
self.setup_course()
|
|
self.setup_user(admin=False, enroll=True, login=False)
|
|
self.verify_response()
|