test_views.py 7.32 KB
Newer Older
1 2 3 4
"""
Tests for the LTI provider views
"""

5
from django.core.urlresolvers import reverse
6 7 8 9
from django.test import TestCase
from django.test.client import RequestFactory
from mock import patch, MagicMock

10
from courseware.testutils import RenderXBlockTestMixin
11
from lti_provider import views, models
12
from lti_provider.signature_validator import SignatureValidator
13
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
14
from student.tests.factories import UserFactory
15
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
16 17 18 19 20 21 22 23 24 25 26


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',
27
    'user_id': u'LTI_User',
28 29
}

30 31 32 33 34 35 36 37 38
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')

39
COURSE_PARAMS = {
40 41
    'course_key': COURSE_KEY,
    'usage_key': USAGE_KEY
42 43 44
}


45
ALL_PARAMS = dict(LTI_DEFAULT_PARAMS.items() + COURSE_PARAMS.items())
46 47 48 49 50 51 52 53 54 55 56 57 58 59


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


60
class LtiTestMixin(object):
61
    """
62
    Mixin for LTI tests
63 64 65
    """
    @patch.dict('django.conf.settings.FEATURES', {'ENABLE_LTI_PROVIDER': True})
    def setUp(self):
66
        super(LtiTestMixin, self).setUp()
67 68
        # Always accept the OAuth signature
        SignatureValidator.verify = MagicMock(return_value=True)
69 70 71 72 73 74
        self.consumer = models.LtiConsumer(
            consumer_name='consumer',
            consumer_key=LTI_DEFAULT_PARAMS['oauth_consumer_key'],
            consumer_secret='secret'
        )
        self.consumer.save()
75

76 77 78 79 80

class LtiLaunchTest(LtiTestMixin, TestCase):
    """
    Tests for the lti_launch view
    """
81
    @patch('lti_provider.views.render_courseware')
82 83
    @patch('lti_provider.views.authenticate_lti_user')
    def test_valid_launch(self, _authenticate, render):
84 85 86
        """
        Verifies that the LTI launch succeeds when passed a valid request.
        """
87
        request = build_launch_request()
88
        views.lti_launch(request, unicode(COURSE_KEY), unicode(USAGE_KEY))
89
        render.assert_called_with(request, USAGE_KEY)
90

91 92
    @patch('lti_provider.views.render_courseware')
    @patch('lti_provider.views.store_outcome_parameters')
93 94
    @patch('lti_provider.views.authenticate_lti_user')
    def test_outcome_service_registered(self, _authenticate, store_params, _render):
95 96 97 98 99 100 101 102 103 104 105
        """
        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)

106 107 108 109
    def launch_with_missing_parameter(self, missing_param):
        """
        Helper method to remove a parameter from the LTI launch and call the view
        """
110
        request = build_launch_request()
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
        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}):
132
            request = build_launch_request()
133 134 135 136 137 138 139 140 141
            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)
142
        request = build_launch_request()
143 144
        response = views.lti_launch(request, None, None)
        self.assertEqual(response.status_code, 403)
145
        self.assertEqual(response.status_code, 403)
146

147 148
    @patch('lti_provider.views.render_courseware')
    def test_lti_consumer_record_supplemented_with_guid(self, _render):
149 150 151
        SignatureValidator.verify = MagicMock(return_value=False)
        request = build_launch_request()
        request.POST.update(LTI_OPTIONAL_PARAMS)
152
        with self.assertNumQueries(4):
153
            views.lti_launch(request, None, None)
154 155 156
        consumer = models.LtiConsumer.objects.get(
            consumer_key=LTI_DEFAULT_PARAMS['oauth_consumer_key']
        )
157
        self.assertEqual(consumer.instance_guid, u'consumer instance guid')
158

159

160
class LtiLaunchTestRender(LtiTestMixin, RenderXBlockTestMixin, ModuleStoreTestCase):
161
    """
162
    Tests for the rendering returned by lti_launch view.
163 164
    This class overrides the get_response method, which is used by
    the tests defined in RenderXBlockTestMixin.
165
    """
166
    def get_response(self, url_encoded_params=None):
167
        """
168
        Overridable method to get the response from the endpoint that is being tested.
169
        """
170 171
        lti_launch_url = reverse(
            'lti_provider_launch',
172 173 174 175
            kwargs={
                'course_id': unicode(self.course.id),
                'usage_id': unicode(self.html_block.location)
            }
176
        )
177 178
        if url_encoded_params:
            lti_launch_url += '?' + url_encoded_params
179 180
        SignatureValidator.verify = MagicMock(return_value=True)
        return self.client.post(lti_launch_url, data=LTI_DEFAULT_PARAMS)
181

182 183 184 185 186
    # The following test methods override the base tests for verifying access
    # by unenrolled and unauthenticated students, since there is a discrepancy
    # of access rules between the 2 endpoints (LTI and xBlock_render).
    # TODO fix this access discrepancy to the same underlying data.

187
    def test_unenrolled_student(self):
188 189 190
        """
        Override since LTI allows access to unenrolled students.
        """
191 192 193
        self.setup_course()
        self.setup_user(admin=False, enroll=False, login=True)
        self.verify_response()
194 195

    def test_unauthenticated(self):
196 197 198
        """
        Override since LTI allows access to unauthenticated users.
        """
199 200 201
        self.setup_course()
        self.setup_user(admin=False, enroll=True, login=False)
        self.verify_response()