diff --git a/lti_consumer/plugin/compat.py b/lti_consumer/plugin/compat.py new file mode 100644 index 0000000000000000000000000000000000000000..68994bf45994b3d7558e468eb2c02a513097d95f --- /dev/null +++ b/lti_consumer/plugin/compat.py @@ -0,0 +1,21 @@ +""" +Compatibility layer to isolate core-platform method calls from implementation. +""" + + +def run_xblock_handler(*args, **kwargs): + """ + Import and run `handle_xblock_callback` from LMS + """ + # pylint: disable=import-error,import-outside-toplevel + from lms.djangoapps.courseware.module_render import handle_xblock_callback + return handle_xblock_callback(*args, **kwargs) + + +def run_xblock_handler_noauth(*args, **kwargs): + """ + Import and run `handle_xblock_callback_noauth` from LMS + """ + # pylint: disable=import-error,import-outside-toplevel + from lms.djangoapps.courseware.module_render import handle_xblock_callback_noauth + return handle_xblock_callback_noauth(*args, **kwargs) diff --git a/lti_consumer/plugin/views.py b/lti_consumer/plugin/views.py index 755a559c8d1c62ebf935a28090759fb32ec82b3a..78174a50090221e8debafd443219ed350145ef90 100644 --- a/lti_consumer/plugin/views.py +++ b/lti_consumer/plugin/views.py @@ -7,9 +7,9 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods from opaque_keys.edx.keys import UsageKey -from lms.djangoapps.courseware.module_render import ( # pylint: disable=import-error - handle_xblock_callback, - handle_xblock_callback_noauth, +from lti_consumer.plugin.compat import ( + run_xblock_handler, + run_xblock_handler_noauth, ) @@ -25,13 +25,13 @@ def public_keyset_endpoint(request, usage_id=None): try: usage_key = UsageKey.from_string(usage_id) - return handle_xblock_callback_noauth( + return run_xblock_handler_noauth( request=request, course_id=str(usage_key.course_key), usage_id=str(usage_key), handler='public_keyset_endpoint' ) - except: # pylint: disable=bare-except + except Exception: # pylint: disable=broad-except return HttpResponse(status=404) @@ -49,14 +49,14 @@ def launch_gate_endpoint(request, suffix): request.GET.get('login_hint') ) - return handle_xblock_callback( + return run_xblock_handler( request=request, course_id=str(usage_key.course_key), usage_id=str(usage_key), handler='lti_1p3_launch_callback', suffix=suffix ) - except: # pylint: disable=bare-except + except Exception: # pylint: disable=broad-except return HttpResponse(status=404) @@ -69,11 +69,11 @@ def access_token_endpoint(request, usage_id=None): try: usage_key = UsageKey.from_string(usage_id) - return handle_xblock_callback_noauth( + return run_xblock_handler_noauth( request=request, course_id=str(usage_key.course_key), usage_id=str(usage_key), handler='lti_1p3_access_token' ) - except: # pylint: disable=bare-except + except Exception: # pylint: disable=broad-except return HttpResponse(status=404) diff --git a/lti_consumer/tests/unit/plugin/__init__.py b/lti_consumer/tests/unit/plugin/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lti_consumer/tests/unit/plugin/test_views.py b/lti_consumer/tests/unit/plugin/test_views.py new file mode 100644 index 0000000000000000000000000000000000000000..21345f973626a1ce5161b4e880b9423c6bfca2a3 --- /dev/null +++ b/lti_consumer/tests/unit/plugin/test_views.py @@ -0,0 +1,133 @@ +""" +Tests for LTI 1.3 endpoint views. +""" +from mock import patch + +from django.http import HttpResponse +from django.test.testcases import TestCase + + +class TestLti1p3KeysetEndpoint(TestCase): + """ + Test `public_keyset_endpoint` method. + """ + def setUp(self): + super(TestLti1p3KeysetEndpoint, self).setUp() + + self.location = 'block-v1:course+test+2020+type@problem+block@test' + self.url = '/lti_consumer/v1/public_keysets/{}'.format(self.location) + + # Patch settings calls to LMS method + xblock_handler_patcher = patch( + 'lti_consumer.plugin.views.run_xblock_handler_noauth', + return_value=HttpResponse() + ) + self.addCleanup(xblock_handler_patcher.stop) + self._mock_xblock_handler = xblock_handler_patcher.start() + + def test_public_keyset_endpoint(self): + """ + Check that the keyset endpoint maps correctly to the + `public_keyset_endpoint` XBlock handler endpoint. + """ + response = self.client.get(self.url) + + # Check response + self.assertEqual(response.status_code, 200) + # Check function call arguments + self._mock_xblock_handler.assert_called_once() + kwargs = self._mock_xblock_handler.call_args.kwargs + self.assertEqual(kwargs['usage_id'], self.location) + self.assertEqual(kwargs['handler'], 'public_keyset_endpoint') + + def test_invalid_usage_key(self): + """ + Check invalid methods yield HTTP code 404. + """ + response = self.client.get('/lti_consumer/v1/public_keysets/invalid-key') + self.assertEqual(response.status_code, 404) + + +class TestLti1p3LaunchGateEndpoint(TestCase): + """ + Test `launch_gate_endpoint` method. + """ + def setUp(self): + super(TestLti1p3LaunchGateEndpoint, self).setUp() + + self.location = 'block-v1:course+test+2020+type@problem+block@test' + self.url = '/lti_consumer/v1/launch/' + self.request = {'login_hint': self.location} + + # Patch settings calls to LMS method + xblock_handler_patcher = patch( + 'lti_consumer.plugin.views.run_xblock_handler', + return_value=HttpResponse() + ) + self.addCleanup(xblock_handler_patcher.stop) + self._mock_xblock_handler = xblock_handler_patcher.start() + + def test_launch_gate(self): + """ + Check that the launch endpoint correctly maps to the + `lti_1p3_launch_callback` XBlock handler. + """ + response = self.client.get(self.url, self.request) + + # Check response + self.assertEqual(response.status_code, 200) + # Check function call arguments + self._mock_xblock_handler.assert_called_once() + kwargs = self._mock_xblock_handler.call_args.kwargs + self.assertEqual(kwargs['usage_id'], self.location) + self.assertEqual(kwargs['handler'], 'lti_1p3_launch_callback') + + def test_invalid_usage_key(self): + """ + Check that passing a invalid login_hint yields HTTP code 404. + """ + self._mock_xblock_handler.side_effect = Exception() + response = self.client.get(self.url, self.request) + self.assertEqual(response.status_code, 404) + + +class TestLti1p3AccessTokenEndpoint(TestCase): + """ + Test `access_token_endpoint` method. + """ + def setUp(self): + super(TestLti1p3AccessTokenEndpoint, self).setUp() + + self.location = 'block-v1:course+test+2020+type@problem+block@test' + self.url = '/lti_consumer/v1/token/{}'.format(self.location) + + # Patch settings calls to LMS method + xblock_handler_patcher = patch( + 'lti_consumer.plugin.views.run_xblock_handler_noauth', + return_value=HttpResponse() + ) + self.addCleanup(xblock_handler_patcher.stop) + self._mock_xblock_handler = xblock_handler_patcher.start() + + def test_access_token_endpoint(self): + """ + Check that the keyset endpoint is correctly mapping to the + `lti_1p3_access_token` XBlock handler. + """ + response = self.client.post(self.url) + + # Check response + self.assertEqual(response.status_code, 200) + # Check function call arguments + self._mock_xblock_handler.assert_called_once() + kwargs = self._mock_xblock_handler.call_args.kwargs + self.assertEqual(kwargs['usage_id'], self.location) + self.assertEqual(kwargs['handler'], 'lti_1p3_access_token') + + def test_invalid_usage_key(self): + """ + Check invalid methods yield HTTP code 404. + """ + self._mock_xblock_handler.side_effect = Exception() + response = self.client.post(self.url) + self.assertEqual(response.status_code, 404) diff --git a/test.py b/test.py index efce05d858e922d0186df9fe3db7e6dd2baaec28..eb32b0414d31a1f1ff415be54bc7f526c47f4d52 100644 --- a/test.py +++ b/test.py @@ -8,7 +8,7 @@ import os import sys if __name__ == '__main__': - os.environ.setdefault('DJANGO_SETTINGS_MODULE', u'workbench.settings') + os.environ.setdefault('DJANGO_SETTINGS_MODULE', u'test_settings') try: from django.conf import settings # pylint: disable=wrong-import-position diff --git a/test_settings.py b/test_settings.py new file mode 100644 index 0000000000000000000000000000000000000000..e6e2f7fa10b6833d1007b576207f09dcc60e54bf --- /dev/null +++ b/test_settings.py @@ -0,0 +1,11 @@ +""" +Custom testing settings for testing views +""" +from workbench.settings import * + + +# Usage id pattern (from edx-platform) +USAGE_ID_PATTERN = r'(?P<usage_id>(?:i4x://?[^/]+/[^/]+/[^/]+/[^@]+(?:@[^/]+)?)|(?:[^/]+))' + +# Keep settings, use different ROOT_URLCONF +ROOT_URLCONF = 'test_urls' diff --git a/test_urls.py b/test_urls.py new file mode 100644 index 0000000000000000000000000000000000000000..66f5f749dd579ff0866ae9391bbee5191546f3b4 --- /dev/null +++ b/test_urls.py @@ -0,0 +1,9 @@ +""" +Custom URL patterns for testing +""" +from django.conf.urls import include, re_path + +urlpatterns = [ + re_path(r'^', include('workbench.urls')), + re_path(r'^', include('lti_consumer.plugin.urls')), +]