diff --git a/setup.cfg b/setup.cfg index f058d41..4707463 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,7 +7,7 @@ license_file = LICENSE [pycodestyle] max_line_length=120 -exclude=.git,.idea,.tox,venv,env +exclude=.git,.idea,.tox,venv,venv3,env [coverage:run] branch = True diff --git a/tests/__init__.py b/tests/__init__.py index 04c39bd..635490b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,4 +2,3 @@ from .test_grades import TestGrades from .test_names_roles import TestNamesRolesProvisioningService from .test_resource_link import TestResourceLink -from .test_flask_resource_link import TestFlaskResourceLink diff --git a/tests/base.py b/tests/base.py index efde3d1..28b7700 100644 --- a/tests/base.py +++ b/tests/base.py @@ -6,73 +6,35 @@ from unittest.mock import patch except ImportError: from mock import patch -try: - from urllib import quote -except ImportError: - from urllib.parse import quote -from .request import FakeRequest -from .response import FakeResponse -from .tool_config import get_test_tool_conf, TOOL_CONFIG +from .django_mixin import DjangoMixin +from .flask_mixin import FlaskMixin +from .tool_config import TOOL_CONFIG -class TestLinkBase(unittest.TestCase): +class TestLinkBase(DjangoMixin, FlaskMixin, unittest.TestCase): iss = 'replace-me' get_login_data = {} post_login_data = {} - def _make_oidc_login(self, uuid_val=None, tool_conf_cls=None): - tool_conf = get_test_tool_conf(tool_conf_cls) - request = None - login_data = {} - if not uuid_val: - uuid_val = 'test-uuid-1234' - - if self.get_login_data: - request = FakeRequest(get=self.get_login_data) - login_data = self.get_login_data.copy() - elif self.post_login_data: - request = FakeRequest(post=self.post_login_data) - login_data = self.post_login_data.copy() - - with patch('django.shortcuts.redirect') as mock_redirect: - from pylti1p3.contrib.django import DjangoOIDCLogin - with patch.object(DjangoOIDCLogin, "_get_uuid", autospec=True) as get_uuid: - get_uuid.side_effect = lambda x: uuid_val # pylint: disable=unnecessary-lambda - oidc_login = DjangoOIDCLogin(request, tool_conf) - mock_redirect.side_effect = lambda x: FakeResponse(x) # pylint: disable=unnecessary-lambda - launch_url = 'http://lti.django.test/launch/' - response = oidc_login.redirect(launch_url) - - # check cookie data - self.assertEqual(len(response.cookies), 1) - self.assertTrue(('lti1p3-state-' + uuid_val) in response.cookies) - self.assertEqual(response.cookies['lti1p3-state-' + uuid_val]['value'], 'state-' + uuid_val) - - # check session data - self.assertEqual(len(request.session), 1) - self.assertEqual(request.session['lti1p3-nonce-' + uuid_val], True) - - # check redirect_url - redirect_url = response.data - self.assertTrue(redirect_url.startswith(TOOL_CONFIG[login_data['iss']]['auth_login_url'])) - url_params = redirect_url.split('?')[1].split('&') - self.assertTrue(('nonce=' + uuid_val) in url_params) - self.assertTrue(('state=state-' + uuid_val) in url_params) - self.assertTrue(('state=state-' + uuid_val) in url_params) - self.assertTrue('prompt=none' in url_params) - self.assertTrue('response_type=id_token' in url_params) - self.assertTrue(('client_id=' + TOOL_CONFIG[login_data['iss']]['client_id']) in url_params) - self.assertTrue(('login_hint=' + login_data['login_hint']) in url_params) - self.assertTrue(('lti_message_hint=' + login_data['lti_message_hint']) in url_params) - self.assertTrue('scope=openid' in url_params) - self.assertTrue('response_mode=form_post' in url_params) - self.assertTrue(('redirect_uri=' + quote(launch_url, '')) in url_params) - - return tool_conf, request, response - - def _launch(self, request, tool_conf, key_set_url_response=None, force_validation=False): - from pylti1p3.contrib.django import DjangoMessageLaunch - obj = DjangoMessageLaunch(request, tool_conf) + def _make_oidc_login(self, adapter=None, uuid_val=None, tool_conf_cls=None, secure=False): + if adapter == 'flask': + return self._make_flask_oidc_login(uuid_val, tool_conf_cls, secure) + else: + return self._make_django_oidc_login(uuid_val, tool_conf_cls) + + def _get_request(self, login_request, login_response, request_is_secure=False, empty_session=False, + empty_cookies=False, post_data=None, adapter=None): + if adapter == 'flask': + return self._get_flask_request(login_request, login_response, request_is_secure, post_data, + empty_session, empty_cookies) + else: + return self._get_django_request(login_request, login_response, post_data, empty_session, empty_cookies) + + def _launch(self, request, tool_conf, key_set_url_response=None, force_validation=False, adapter=None): + if adapter == 'flask': + obj = self._get_flask_launch_obj(request, tool_conf) + else: + obj = self._get_django_launch_obj(request, tool_conf) obj.set_jwt_verify_options({ 'verify_aud': False, 'verify_exp': False @@ -87,11 +49,14 @@ def _launch(self, request, tool_conf, key_set_url_response=None, force_validatio else: return obj.get_launch_data() - def _launch_with_invalid_jwt_body(self, side_effect, request, tool_conf): - from pylti1p3.contrib.django import DjangoMessageLaunch - with patch.object(DjangoMessageLaunch, "_get_jwt_body", autospec=True) as get_jwt_body: + def _launch_with_invalid_jwt_body(self, side_effect, request, tool_conf, adapter=None): + if adapter == 'flask': + klass = self._get_flask_launch_cls() + else: + klass = self._get_django_launch_cls() + with patch.object(klass, "_get_jwt_body", autospec=True) as get_jwt_body: get_jwt_body.side_effect = side_effect - return self._launch(request, tool_conf, force_validation=True) + return self._launch(request, tool_conf, force_validation=True, adapter=adapter) class TestServicesBase(unittest.TestCase): diff --git a/tests/django_mixin.py b/tests/django_mixin.py new file mode 100644 index 0000000..8755c21 --- /dev/null +++ b/tests/django_mixin.py @@ -0,0 +1,82 @@ +try: + from unittest.mock import patch +except ImportError: + from mock import patch +try: + from urllib import quote +except ImportError: + from urllib.parse import quote +from .request import FakeRequest +from .response import FakeResponse +from .tool_config import get_test_tool_conf, TOOL_CONFIG + + +class DjangoMixin(object): + + def _get_django_request(self, login_request, login_response, post_data=None, + empty_session=False, empty_cookies=False): + session = None if empty_session else login_request.session + cookies = None if empty_cookies else login_response.get_cookies_dict() + post_launch_data = post_data if post_data else self.post_launch_data + return FakeRequest(post=post_launch_data, + cookies=cookies, + session=session) + + def _make_django_oidc_login(self, uuid_val=None, tool_conf_cls=None): + tool_conf = get_test_tool_conf(tool_conf_cls) + request = None + login_data = {} + if not uuid_val: + uuid_val = 'test-uuid-1234' + + if self.get_login_data: + request = FakeRequest(get=self.get_login_data) + login_data = self.get_login_data.copy() + elif self.post_login_data: + request = FakeRequest(post=self.post_login_data) + login_data = self.post_login_data.copy() + + with patch('django.shortcuts.redirect') as mock_redirect: + from pylti1p3.contrib.django import DjangoOIDCLogin + with patch.object(DjangoOIDCLogin, "_get_uuid", autospec=True) as get_uuid: + get_uuid.side_effect = lambda x: uuid_val # pylint: disable=unnecessary-lambda + oidc_login = DjangoOIDCLogin(request, tool_conf) + mock_redirect.side_effect = lambda x: FakeResponse(x) # pylint: disable=unnecessary-lambda + launch_url = 'http://lti.django.test/launch/' + response = oidc_login.redirect(launch_url) + + # check cookie data + self.assertEqual(len(response.cookies), 1) + self.assertTrue(('lti1p3-state-' + uuid_val) in response.cookies) + self.assertEqual(response.cookies['lti1p3-state-' + uuid_val]['value'], 'state-' + uuid_val) + + # check session data + self.assertEqual(len(request.session), 1) + self.assertEqual(request.session['lti1p3-nonce-' + uuid_val], True) + + # check redirect_url + redirect_url = response.data + self.assertTrue(redirect_url.startswith(TOOL_CONFIG[login_data['iss']]['auth_login_url'])) + url_params = redirect_url.split('?')[1].split('&') + self.assertTrue(('nonce=' + uuid_val) in url_params) + self.assertTrue(('state=state-' + uuid_val) in url_params) + self.assertTrue(('state=state-' + uuid_val) in url_params) + self.assertTrue('prompt=none' in url_params) + self.assertTrue('response_type=id_token' in url_params) + self.assertTrue(('client_id=' + TOOL_CONFIG[login_data['iss']]['client_id']) in url_params) + self.assertTrue(('login_hint=' + login_data['login_hint']) in url_params) + self.assertTrue(('lti_message_hint=' + login_data['lti_message_hint']) in url_params) + self.assertTrue('scope=openid' in url_params) + self.assertTrue('response_mode=form_post' in url_params) + self.assertTrue(('redirect_uri=' + quote(launch_url, '')) in url_params) + + return tool_conf, request, response + + def _get_django_launch_obj(self, request, tool_conf): + from pylti1p3.contrib.django import DjangoMessageLaunch + obj = DjangoMessageLaunch(request, tool_conf) + return obj + + def _get_django_launch_cls(self): + from pylti1p3.contrib.django import DjangoMessageLaunch + return DjangoMessageLaunch diff --git a/tests/flask_base.py b/tests/flask_mixin.py similarity index 79% rename from tests/flask_base.py rename to tests/flask_mixin.py index 70e5d30..e55d2c5 100644 --- a/tests/flask_base.py +++ b/tests/flask_mixin.py @@ -1,7 +1,3 @@ -import json -import unittest -import requests_mock - from pylti1p3.contrib.flask import FlaskRequest, FlaskCookieService, \ FlaskSessionService @@ -17,10 +13,24 @@ from .tool_config import get_test_tool_conf, TOOL_CONFIG -class TestFlaskLinkBase(unittest.TestCase): - iss = 'replace-me' +class FlaskMixin(object): + + def get_cookies_dict_from_response(self, response): + cookie_name, cookie_value = response.headers['Set-Cookie']\ + .split(';')[0].split('=') + return {cookie_name: cookie_value} - def _make_oidc_login(self, secure, uuid_val=None, tool_conf_cls=None): + def _get_flask_request(self, login_request, login_response, request_is_secure=False, post_data=None, + empty_session=False, empty_cookies=False): + session = {} if empty_session else login_request.session + cookies = {} if empty_cookies else self.get_cookies_dict_from_response(login_response) + post_launch_data = post_data if post_data else self.post_launch_data + return FlaskRequest(request_data=post_launch_data, + cookies=cookies, + session=session, + request_is_secure=request_is_secure) + + def _make_flask_oidc_login(self, uuid_val=None, tool_conf_cls=None, secure=None): tool_conf = get_test_tool_conf(tool_conf_cls) if not uuid_val: uuid_val = 'test-uuid-1234' @@ -91,27 +101,13 @@ def _make_oidc_login(self, secure, uuid_val=None, tool_conf_cls=None): return tool_conf, request, response - def _launch(self, request, tool_conf, key_set_url_response=None, force_validation=False): + def _get_flask_launch_obj(self, request, tool_conf): from pylti1p3.contrib.flask import FlaskMessageLaunch obj = FlaskMessageLaunch(request, tool_conf, cookie_service=FlaskCookieService(request), session_service=FlaskSessionService(request)) - obj.set_jwt_verify_options({ - 'verify_aud': False, - 'verify_exp': False - }) - - with patch('socket.gethostbyname', return_value="127.0.0.1"): - with requests_mock.Mocker() as m: - key_set_url_text = key_set_url_response if key_set_url_response else json.dumps(self.jwt_canvas_keys) - m.get(TOOL_CONFIG[self.iss]['key_set_url'], text=key_set_url_text) - if force_validation: - return obj.validate() - else: - return obj.get_launch_data() + return obj - def _launch_with_invalid_jwt_body(self, side_effect, request, tool_conf): + def _get_flask_launch_cls(self): from pylti1p3.contrib.flask import FlaskMessageLaunch - with patch.object(FlaskMessageLaunch, "_get_jwt_body", autospec=True) as get_jwt_body: - get_jwt_body.side_effect = side_effect - return self._launch(request, tool_conf, force_validation=True) + return FlaskMessageLaunch diff --git a/tests/test_flask_resource_link.py b/tests/test_flask_resource_link.py deleted file mode 100644 index 0c59573..0000000 --- a/tests/test_flask_resource_link.py +++ /dev/null @@ -1,327 +0,0 @@ -from parameterized import parameterized - -from pylti1p3.contrib.flask import FlaskRequest -from pylti1p3.exception import LtiException -from .flask_base import TestFlaskLinkBase -from .tool_config import ToolConfDeprecated - - -class TestFlaskResourceLink(TestFlaskLinkBase): - iss = 'https://canvas.instructure.com' - jwt_canvas_keys = { - "keys": [ - { - "kty": "RSA", - "e": "AQAB", - "n": "uX1MpfEMQCBUMcj0sBYI-iFaG5Nodp3C6OlN8uY60fa5zSBd83-iIL3n_qzZ8VCluuTLfB7rrV_tiX727XIEqQ", - "kid": "2018-05-18T22:33:20Z" - }, { - "kty": "RSA", - "e": "AQAB", - "n": "uX1MpfEMQCBUMcj0sBYI-iFaG5Nodp3C6OlN8uY60fa5zSBd83-iIL3n_qzZ8VCluuTLfB7rrV_tiX727XIEqQ", - "kid": "2018-06-18T22:33:20Z" - }, { - "kty": "RSA", - "e": "AQAB", - "n": "uX1MpfEMQCBUMcj0sBYI-iFaG5Nodp3C6OlN8uY60fa5zSBd83-iIL3n_qzZ8VCluuTLfB7rrV_tiX727XIEqQ", - "kid": "2018-07-18T22:33:20Z" - } - ] - } - - login_data = { - 'iss': iss, - 'login_hint': '86157096483e6b3a50bfedc6bac902c0b20a824f', - 'target_link_uri': 'http://lti.django.test/launch/', - 'lti_message_hint': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ2ZXJpZmllciI6Ijg0NjMxZjc1Z' - 'GYxNmNiZjNmYTM5YzEwMzk4YTg0M2U1NTAwZTc5MTU2OTBhN2RjYTJhNGMzMTJjYjR' - 'jOWU0YWY5NzE2MWVhYjg4ODhmOWJlNDc2MmViNzUzZDE5ZmI3YWU5N2I2MjAxZWZjM' - 'jRmODY4NWE3NjJmY2U0ZWU4MDk4IiwiY2FudmFzX2RvbWFpbiI6ImNhbnZhcy5kb2N' - 'rZXIiLCJjb250ZXh0X3R5cGUiOiJDb3Vyc2UiLCJjb250ZXh0X2lkIjoxMDAwMDAwM' - 'DAwMDAwMSwiZXhwIjoxNTY1NDQyMzcwfQ.B1Lddgthaa-YBT4-Lkm3OM_noETl3dIz' - '5E14YWJ8m_Q' - } - - launch_data = { - 'utf8': '%E2%9C%93', - 'authenticity_token': 'oOOlsiqy2nFHP5wgWIKWSEoHKYDZg0u%2BCRKC3BWuFsORmeT2HMC%2BASxQzEoW0' - 'KdnfnZe6ovmOe9gVOqYPth5mw%3D%3D', - 'state': 'state-test-uuid-1234', - 'id_token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjIwMTgtMDYtMThUMjI6MzM6MjBaIn0.' - 'eyJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS9jbGFpbS9tZXNzYWdlX3R5cGUi' - 'OiJMdGlSZXNvdXJjZUxpbmtSZXF1ZXN0IiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3Bl' - 'Yy9sdGkvY2xhaW0vdmVyc2lvbiI6IjEuMy4wIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcv' - 'c3BlYy9sdGkvY2xhaW0vcmVzb3VyY2VfbGluayI6eyJpZCI6IjRkZGUwNWU4Y2ExOTczYmNjYTli' - 'ZmZjMTNlMTU0ODgyMGVlZTkzYTMiLCJkZXNjcmlwdGlvbiI6bnVsbCwidGl0bGUiOm51bGwsInZh' - 'bGlkYXRpb25fY29udGV4dCI6bnVsbCwiZXJyb3JzIjp7ImVycm9ycyI6e319fSwiYXVkIjoiMTAw' - 'MDAwMDAwMDAwMDQiLCJhenAiOiIxMDAwMDAwMDAwMDAwNCIsImh0dHBzOi8vcHVybC5pbXNnbG9i' - 'YWwub3JnL3NwZWMvbHRpL2NsYWltL2RlcGxveW1lbnRfaWQiOiI2Ojg4NjVhYTA1YjRiNzliNjRh' - 'OTFhODYwNDJlNDNhZjVlYThhZTc5ZWIiLCJleHAiOjE1NjU0NDU2NzAsImlhdCI6MTU2NTQ0MjA3' - 'MCwiaXNzIjoiaHR0cHM6Ly9jYW52YXMuaW5zdHJ1Y3R1cmUuY29tIiwibm9uY2UiOiJ0ZXN0LXV1' - 'aWQtMTIzNCIsInN1YiI6ImE0NDVjYTk5LTFhNjQtNDY5Ny05YmZhLTUwOGExMTgyNDVlYSIsImh0' - 'dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL3RhcmdldF9saW5rX3VyaSI6' - 'Imh0dHA6Ly9sdGkuZGphbmdvLnRlc3QvbGF1bmNoLyIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwu' - 'b3JnL3NwZWMvbHRpL2NsYWltL2NvbnRleHQiOnsiaWQiOiI0ZGRlMDVlOGNhMTk3M2JjY2E5YmZm' - 'YzEzZTE1NDg4MjBlZWU5M2EzIiwibGFiZWwiOiJUZXN0IiwidGl0bGUiOiJUZXN0IiwidHlwZSI6' - 'WyJodHRwOi8vcHVybC5pbXNnbG9iYWwub3JnL3ZvY2FiL2xpcy92Mi9jb3Vyc2UjQ291cnNlT2Zm' - 'ZXJpbmciXSwidmFsaWRhdGlvbl9jb250ZXh0IjpudWxsLCJlcnJvcnMiOnsiZXJyb3JzIjp7fX19' - 'LCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS9jbGFpbS90b29sX3BsYXRmb3Jt' - 'Ijp7Imd1aWQiOiJDZUFEeks3aHNQWWZ6bXlDN0xUTDhjcHpaSVNOZHBXalZnMVVaakxZOmNhbnZh' - 'cy1sbXMiLCJuYW1lIjoiRG1pdHJ5T3JnIiwidmVyc2lvbiI6ImNsb3VkIiwicHJvZHVjdF9mYW1p' - 'bHlfY29kZSI6ImNhbnZhcyIsInZhbGlkYXRpb25fY29udGV4dCI6bnVsbCwiZXJyb3JzIjp7ImVy' - 'cm9ycyI6e319fSwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGkvY2xhaW0vbGF1' - 'bmNoX3ByZXNlbnRhdGlvbiI6eyJkb2N1bWVudF90YXJnZXQiOiJpZnJhbWUiLCJoZWlnaHQiOm51' - 'bGwsIndpZHRoIjpudWxsLCJyZXR1cm5fdXJsIjoiaHR0cDovL2NhbnZhcy5kb2NrZXIvY291cnNl' - 'cy8xL2V4dGVybmFsX2NvbnRlbnQvc3VjY2Vzcy9leHRlcm5hbF90b29sX3JlZGlyZWN0IiwibG9j' - 'YWxlIjoiZW4iLCJ2YWxpZGF0aW9uX2NvbnRleHQiOm51bGwsImVycm9ycyI6eyJlcnJvcnMiOnt9' - 'fX0sImxvY2FsZSI6ImVuIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGkvY2xh' - 'aW0vcm9sZXMiOlsiaHR0cDovL3B1cmwuaW1zZ2xvYmFsLm9yZy92b2NhYi9saXMvdjIvaW5zdGl0' - 'dXRpb24vcGVyc29uI0FkbWluaXN0cmF0b3IiLCJodHRwOi8vcHVybC5pbXNnbG9iYWwub3JnL3Zv' - 'Y2FiL2xpcy92Mi9zeXN0ZW0vcGVyc29uI1N5c0FkbWluIiwiaHR0cDovL3B1cmwuaW1zZ2xvYmFs' - 'Lm9yZy92b2NhYi9saXMvdjIvc3lzdGVtL3BlcnNvbiNVc2VyIl0sImh0dHBzOi8vcHVybC5pbXNn' - 'bG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL2N1c3RvbSI6eyJlbWFpbCI6ImFkbWluQGFkbWluLmNv' - 'bSIsInVzZXJfaWQiOjJ9LCJlcnJvcnMiOnsiZXJyb3JzIjp7fX0sImh0dHBzOi8vcHVybC5pbXNn' - 'bG9iYWwub3JnL3NwZWMvbHRpLWFncy9jbGFpbS9lbmRwb2ludCI6eyJzY29wZSI6WyJodHRwczov' - 'L3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS1hZ3Mvc2NvcGUvc2NvcmUiLCJodHRwczovL3B1' - 'cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS1hZ3Mvc2NvcGUvcmVzdWx0LnJlYWRvbmx5IiwiaHR0' - 'cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGktYWdzL3Njb3BlL2xpbmVpdGVtLnJlYWRv' - 'bmx5IiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGktYWdzL3Njb3BlL2xpbmVp' - 'dGVtIl0sImxpbmVpdGVtcyI6Imh0dHA6Ly9jYW52YXMuZG9ja2VyL2FwaS9sdGkvY291cnNlcy8x' - 'L2xpbmVfaXRlbXMiLCJ2YWxpZGF0aW9uX2NvbnRleHQiOm51bGwsImVycm9ycyI6eyJlcnJvcnMi' - 'Ont9fX0sImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpLW5ycHMvY2xhaW0vbmFt' - 'ZXNyb2xlc2VydmljZSI6eyJjb250ZXh0X21lbWJlcnNoaXBzX3VybCI6Imh0dHA6Ly9jYW52YXMu' - 'ZG9ja2VyL2FwaS9sdGkvY291cnNlcy8xL25hbWVzX2FuZF9yb2xlcyIsInNlcnZpY2VfdmVyc2lv' - 'bnMiOlsiMi4wIl0sInZhbGlkYXRpb25fY29udGV4dCI6bnVsbCwiZXJyb3JzIjp7ImVycm9ycyI6' - 'e319fX0.XR7ED7t3GVksBKO12gh99dvTgEhWtwcEgmJUqrdeU9UYGKyU7AX8r3hpmsonyItZnTOH' - 'wuITv7Y0ejn033RypQ' - } - - expected_message_launch_data = { - 'nonce': 'test-uuid-1234', - 'https://purl.imsglobal.org/spec/lti/claim/tool_platform': { - 'errors': {'errors': {}}, 'name': 'DmitryOrg', - 'version': 'cloud', - 'product_family_code': 'canvas', - 'guid': 'CeADzK7hsPYfzmyC7LTL8cpzZISNdpWjVg1UZjLY:canvas-lms', - 'validation_context': None}, - 'https://purl.imsglobal.org/spec/lti/claim/context': { - 'errors': { - 'errors': {} - }, - 'title': 'Test', - 'label': 'Test', - 'type': ['http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering'], - 'id': '4dde05e8ca1973bcca9bffc13e1548820eee93a3', - 'validation_context': None}, 'errors': {'errors': {}}, - 'aud': '10000000000004', - 'https://purl.imsglobal.org/spec/lti/claim/version': '1.3.0', - 'iss': 'https://canvas.instructure.com', - 'https://purl.imsglobal.org/spec/lti/claim/roles': [ - 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator', - 'http://purl.imsglobal.org/vocab/lis/v2/system/person#SysAdmin', - 'http://purl.imsglobal.org/vocab/lis/v2/system/person#User'], - 'https://purl.imsglobal.org/spec/lti/claim/custom': {'user_id': 2, 'email': 'admin@admin.com'}, - 'https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice': { - 'context_memberships_url': 'http://canvas.docker/api/lti/courses/1/names_and_roles', - 'service_versions': ['2.0'], 'errors': {'errors': {}}, 'validation_context': None}, 'locale': 'en', - 'https://purl.imsglobal.org/spec/lti/claim/resource_link': { - 'errors': {'errors': {}}, - 'validation_context': None, 'title': None, - 'id': '4dde05e8ca1973bcca9bffc13e1548820eee93a3', - 'description': None}, - 'https://purl.imsglobal.org/spec/lti/claim/message_type': 'LtiResourceLinkRequest', - 'https://purl.imsglobal.org/spec/lti/claim/deployment_id': '6:8865aa05b4b79b64a91a86042e43af5ea8ae79eb', - 'iat': 1565442070, - 'azp': '10000000000004', - 'exp': 1565445670, - 'https://purl.imsglobal.org/spec/lti-ags/claim/endpoint': { - 'scope': ['https://purl.imsglobal.org/spec/lti-ags/scope/score', - 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', - 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', - 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'], - 'lineitems': 'http://canvas.docker/api/lti/courses/1/line_items', - 'errors': {'errors': {}}, - 'validation_context': None - }, - 'https://purl.imsglobal.org/spec/lti/claim/launch_presentation': { - 'errors': {'errors': {}}, - 'locale': 'en', - 'height': None, - 'width': None, - 'document_target': 'iframe', - 'return_url': 'http://canvas.docker/courses/1/external_content/success/external_tool_redirect', - 'validation_context': None}, - 'https://purl.imsglobal.org/spec/lti/claim/target_link_uri': 'http://lti.django.test/launch/', - 'sub': 'a445ca99-1a64-4697-9bfa-508a118245ea' - } - - def get_cookies_dict_from_response(self, response): - cookie_name, cookie_value = response.headers['Set-Cookie']\ - .split(';')[0].split('=') - return {cookie_name: cookie_value} - - @parameterized.expand([['base', None], ['tool_conf_deprecated', ToolConfDeprecated]]) - def test_res_link_launch_success(self, name, tool_conf_cls): # pylint: disable=unused-argument - tool_conf, login_request, login_response = self._make_oidc_login(tool_conf_cls=tool_conf_cls, secure=False) - - launch_request = FlaskRequest(request_data=self.launch_data, - cookies=self.get_cookies_dict_from_response(login_response), - session=login_request.session, - request_is_secure=False) - message_launch_data = self._launch(launch_request, tool_conf) - self.assertDictEqual(message_launch_data, self.expected_message_launch_data) - - @parameterized.expand([['base', None], ['tool_conf_deprecated', ToolConfDeprecated]]) - def test_res_link_secure_launch_success(self, name, tool_conf_cls): # pylint: disable=unused-argument - tool_conf, login_request, login_response = self._make_oidc_login(tool_conf_cls=tool_conf_cls, secure=True) - - launch_request = FlaskRequest(request_data=self.launch_data, - cookies=self.get_cookies_dict_from_response(login_response), - session=login_request.session, - request_is_secure=True) - message_launch_data = self._launch(launch_request, tool_conf) - self.assertDictEqual(message_launch_data, self.expected_message_launch_data) - - def test_res_link_launch_invalid_public_key(self): - tool_conf, login_request, login_response = self._make_oidc_login(secure=False) - - launch_request = FlaskRequest(request_data=self.launch_data, - cookies=self.get_cookies_dict_from_response(login_response), - session=login_request.session, - request_is_secure=False) - with self.assertRaisesRegexp(LtiException, 'Invalid response'): # pylint: disable=deprecated-method - self._launch(launch_request, tool_conf, 'invalid_key_set') - - def test_res_link_launch_invalid_state(self): - tool_conf, login_request, login_response = self._make_oidc_login(secure=False) - - post_data = self.launch_data.copy() - post_data.pop('state', None) - - launch_request = FlaskRequest(request_data=post_data, - cookies=self.get_cookies_dict_from_response(login_response), - session=login_request.session, - request_is_secure=False) - with self.assertRaisesRegexp(LtiException, 'Missing state param'): # pylint: disable=deprecated-method - self._launch(launch_request, tool_conf) - - launch_request = FlaskRequest(request_data=self.launch_data, - session=login_request.session, - cookies={}, - request_is_secure=False) - with self.assertRaisesRegexp(LtiException, 'State not found'): # pylint: disable=deprecated-method - self._launch(launch_request, tool_conf) - - def test_res_link_launch_invalid_jwt_format(self): - tool_conf, login_request, login_response = self._make_oidc_login(secure=False) - - post_data = self.launch_data.copy() - post_data['id_token'] += '.absjdbasdj' - - launch_request = FlaskRequest(request_data=post_data, - cookies=self.get_cookies_dict_from_response(login_response), - session=login_request.session, - request_is_secure=False) - with self.assertRaisesRegexp(LtiException, 'Invalid id_token'): # pylint: disable=deprecated-method - self._launch(launch_request, tool_conf) - - post_data = self.launch_data.copy() - post_data['id_token'] = 'jbafjjsdbjasdabsjdbasdj1212121212.sdfhdhsf.sdfdsfdsf' - - launch_request = FlaskRequest(request_data=post_data, - cookies=self.get_cookies_dict_from_response(login_response), - session=login_request.session, - request_is_secure=False) - with self.assertRaisesRegexp(LtiException, 'Invalid JWT format'): # pylint: disable=deprecated-method - self._launch(launch_request, tool_conf) - - def test_res_link_launch_invalid_jwt_signature(self): - tool_conf, login_request, login_response = self._make_oidc_login(secure=False) - - post_data = self.launch_data.copy() - post_data['id_token'] += 'jbafjjsdbjasdabsjdbasdj' - - launch_request = FlaskRequest(request_data=post_data, - cookies=self.get_cookies_dict_from_response(login_response), - session=login_request.session, - request_is_secure=False) - with self.assertRaisesRegexp(LtiException, "Can't decode id_token"): # pylint: disable=deprecated-method - self._launch(launch_request, tool_conf) - - def _get_data_without_nonce(self, *args): # pylint: disable=unused-argument - message_launch_data = self.expected_message_launch_data.copy() - message_launch_data.pop('nonce', None) - return message_launch_data - - def _get_data_with_invalid_aud(self, *args): # pylint: disable=unused-argument - message_launch_data = self.expected_message_launch_data.copy() - message_launch_data['aud'] = 'dsfsdfsdfsdfsd' - return message_launch_data - - def _get_data_with_invalid_deployment(self, *args): # pylint: disable=unused-argument - message_launch_data = self.expected_message_launch_data.copy() - message_launch_data['https://purl.imsglobal.org/spec/lti/claim/deployment_id'] = 'dsfsdfsdfsdfsd' - return message_launch_data - - def _get_data_with_invalid_message(self, *args): # pylint: disable=unused-argument - message_launch_data = self.expected_message_launch_data.copy() - message_launch_data['https://purl.imsglobal.org/spec/lti/claim/version'] = '1.2.0' - return message_launch_data - - def test_res_link_launch_invalid_nonce(self): - - tool_conf, login_request, login_response = self._make_oidc_login(secure=False) - - post_data = self.launch_data.copy() - launch_request = FlaskRequest(request_data=post_data, - cookies=self.get_cookies_dict_from_response(login_response), - session=login_request.session, - request_is_secure=False) - - with self.assertRaisesRegexp(LtiException, '"nonce" is empty'): # pylint: disable=deprecated-method - self._launch_with_invalid_jwt_body(self._get_data_without_nonce, launch_request, tool_conf) - - launch_request = FlaskRequest(request_data=post_data, - cookies=self.get_cookies_dict_from_response(login_response), - session={}, - request_is_secure=False) - - with self.assertRaisesRegexp(LtiException, "Invalid Nonce"): # pylint: disable=deprecated-method - self._launch(launch_request, tool_conf) - - def test_res_link_launch_invalid_registration(self): - tool_conf, login_request, login_response = self._make_oidc_login(secure=False) - - post_data = self.launch_data.copy() - launch_request = FlaskRequest(request_data=post_data, - cookies=self.get_cookies_dict_from_response(login_response), - session=login_request.session, - request_is_secure=False) - - # pylint: disable=deprecated-method - with self.assertRaisesRegexp(LtiException, 'Client id not registered for this issuer'): - self._launch_with_invalid_jwt_body(self._get_data_with_invalid_aud, launch_request, tool_conf) - - def test_res_link_launch_invalid_deployment(self): - tool_conf, login_request, login_response = self._make_oidc_login(secure=False) - - post_data = self.launch_data.copy() - launch_request = FlaskRequest(request_data=post_data, - cookies=self.get_cookies_dict_from_response(login_response), - session=login_request.session, - request_is_secure=False) - - with self.assertRaisesRegexp(Exception, 'Unable to find deployment'): # pylint: disable=deprecated-method - self._launch_with_invalid_jwt_body(self._get_data_with_invalid_deployment, launch_request, tool_conf) - - def test_res_link_launch_invalid_message(self): - tool_conf, login_request, login_response = self._make_oidc_login(secure=False) - - post_data = self.launch_data.copy() - launch_request = FlaskRequest(request_data=post_data, - cookies=self.get_cookies_dict_from_response(login_response), - session=login_request.session, - request_is_secure=False) - - with self.assertRaisesRegexp(LtiException, 'Incorrect version'): # pylint: disable=deprecated-method - self._launch_with_invalid_jwt_body(self._get_data_with_invalid_message, launch_request, tool_conf) diff --git a/tests/test_resource_link.py b/tests/test_resource_link.py index 863b818..15ba03e 100644 --- a/tests/test_resource_link.py +++ b/tests/test_resource_link.py @@ -1,6 +1,5 @@ from parameterized import parameterized from pylti1p3.exception import LtiException -from .request import FakeRequest from .base import TestLinkBase from .tool_config import ToolConfDeprecated @@ -154,74 +153,70 @@ class TestResourceLink(TestLinkBase): 'sub': 'a445ca99-1a64-4697-9bfa-508a118245ea' } - @parameterized.expand([['base', None], ['tool_conf_deprecated', ToolConfDeprecated]]) - def test_res_link_launch_success(self, name, tool_conf_cls): # pylint: disable=unused-argument - tool_conf, login_request, login_response = self._make_oidc_login(tool_conf_cls=tool_conf_cls) - - launch_request = FakeRequest(post=self.post_launch_data, - cookies=login_response.get_cookies_dict(), - session=login_request.session) - message_launch_data = self._launch(launch_request, tool_conf) + @parameterized.expand([['django_base_non_secure', 'django', False, None], + ['flask_base_non_secure', 'flask', False, None], + ['flask_base_secure', 'flask', True, None], + ['django_tool_conf_deprecated_non_secure', 'django', False, ToolConfDeprecated], + ['flask_tool_conf_deprecated_non_secure', 'flask', False, ToolConfDeprecated], + ['flask_tool_conf_deprecated_secure', 'flask', True, ToolConfDeprecated]]) + def test_res_link_launch_success(self, name, adapter, secure, tool_conf_cls): # pylint: disable=unused-argument + tool_conf, login_request, login_response = self._make_oidc_login(adapter=adapter, tool_conf_cls=tool_conf_cls, + secure=secure) + launch_request = self._get_request(login_request, login_response, request_is_secure=secure, adapter=adapter) + message_launch_data = self._launch(launch_request, tool_conf, adapter=adapter) self.assertDictEqual(message_launch_data, self.expected_message_launch_data) - def test_res_link_launch_invalid_public_key(self): - tool_conf, login_request, login_response = self._make_oidc_login() + @parameterized.expand([['django'], ['flask']]) + def test_res_link_launch_invalid_public_key(self, adapter): + tool_conf, login_request, login_response = self._make_oidc_login(adapter=adapter) - launch_request = FakeRequest(post=self.post_launch_data, - cookies=login_response.get_cookies_dict(), - session=login_request.session) + launch_request = self._get_request(login_request, login_response, adapter=adapter) with self.assertRaisesRegexp(LtiException, 'Invalid response'): # pylint: disable=deprecated-method - self._launch(launch_request, tool_conf, 'invalid_key_set') + self._launch(launch_request, tool_conf, 'invalid_key_set', adapter=adapter) - def test_res_link_launch_invalid_state(self): - tool_conf, login_request, login_response = self._make_oidc_login() + @parameterized.expand([['django'], ['flask']]) + def test_res_link_launch_invalid_state(self, adapter): + tool_conf, login_request, login_response = self._make_oidc_login(adapter=adapter) post_data = self.post_launch_data.copy() post_data.pop('state', None) - launch_request = FakeRequest(post=post_data, - cookies=login_response.get_cookies_dict(), - session=login_request.session) + launch_request = self._get_request(login_request, login_response, post_data=post_data, adapter=adapter) with self.assertRaisesRegexp(LtiException, 'Missing state param'): # pylint: disable=deprecated-method - self._launch(launch_request, tool_conf) + self._launch(launch_request, tool_conf, adapter=adapter) - launch_request = FakeRequest(post=self.post_launch_data, - session=login_request.session) + launch_request = self._get_request(login_request, login_response, empty_cookies=True, adapter=adapter) with self.assertRaisesRegexp(LtiException, 'State not found'): # pylint: disable=deprecated-method - self._launch(launch_request, tool_conf) + self._launch(launch_request, tool_conf, adapter=adapter) - def test_res_link_launch_invalid_jwt_format(self): - tool_conf, login_request, login_response = self._make_oidc_login() + @parameterized.expand([['django'], ['flask']]) + def test_res_link_launch_invalid_jwt_format(self, adapter): + tool_conf, login_request, login_response = self._make_oidc_login(adapter=adapter) post_data = self.post_launch_data.copy() post_data['id_token'] += '.absjdbasdj' - launch_request = FakeRequest(post=post_data, - cookies=login_response.get_cookies_dict(), - session=login_request.session) + launch_request = self._get_request(login_request, login_response, post_data=post_data, adapter=adapter) with self.assertRaisesRegexp(LtiException, 'Invalid id_token'): # pylint: disable=deprecated-method - self._launch(launch_request, tool_conf) + self._launch(launch_request, tool_conf, adapter=adapter) post_data = self.post_launch_data.copy() post_data['id_token'] = 'jbafjjsdbjasdabsjdbasdj1212121212.sdfhdhsf.sdfdsfdsf' - launch_request = FakeRequest(post=post_data, - cookies=login_response.get_cookies_dict(), - session=login_request.session) + launch_request = self._get_request(login_request, login_response, post_data=post_data, adapter=adapter) with self.assertRaisesRegexp(LtiException, 'Invalid JWT format'): # pylint: disable=deprecated-method - self._launch(launch_request, tool_conf) + self._launch(launch_request, tool_conf, adapter=adapter) - def test_res_link_launch_invalid_jwt_signature(self): - tool_conf, login_request, login_response = self._make_oidc_login() + @parameterized.expand([['django'], ['flask']]) + def test_res_link_launch_invalid_jwt_signature(self, adapter): + tool_conf, login_request, login_response = self._make_oidc_login(adapter=adapter) post_data = self.post_launch_data.copy() post_data['id_token'] += 'jbafjjsdbjasdabsjdbasdj' - launch_request = FakeRequest(post=post_data, - cookies=login_response.get_cookies_dict(), - session=login_request.session) + launch_request = self._get_request(login_request, login_response, post_data=post_data, adapter=adapter) with self.assertRaisesRegexp(LtiException, "Can't decode id_token"): # pylint: disable=deprecated-method - self._launch(launch_request, tool_conf) + self._launch(launch_request, tool_conf, adapter=adapter) def _get_data_without_nonce(self, *args): # pylint: disable=unused-argument message_launch_data = self.expected_message_launch_data.copy() @@ -243,54 +238,53 @@ def _get_data_with_invalid_message(self, *args): # pylint: disable=unused-argum message_launch_data['https://purl.imsglobal.org/spec/lti/claim/version'] = '1.2.0' return message_launch_data - def test_res_link_launch_invalid_nonce(self): + @parameterized.expand([['django'], ['flask']]) + def test_res_link_launch_invalid_nonce(self, adapter): - tool_conf, login_request, login_response = self._make_oidc_login() + tool_conf, login_request, login_response = self._make_oidc_login(adapter=adapter) post_data = self.post_launch_data.copy() - launch_request = FakeRequest(post=post_data, - cookies=login_response.get_cookies_dict(), - session=login_request.session) + launch_request = self._get_request(login_request, login_response, post_data=post_data, adapter=adapter) with self.assertRaisesRegexp(LtiException, '"nonce" is empty'): # pylint: disable=deprecated-method - self._launch_with_invalid_jwt_body(self._get_data_without_nonce, launch_request, tool_conf) + self._launch_with_invalid_jwt_body(self._get_data_without_nonce, launch_request, tool_conf, adapter=adapter) - launch_request = FakeRequest(post=post_data, - cookies=login_response.get_cookies_dict()) + launch_request = self._get_request(login_request, login_response, post_data=post_data, empty_session=True, + adapter=adapter) with self.assertRaisesRegexp(LtiException, "Invalid Nonce"): # pylint: disable=deprecated-method - self._launch(launch_request, tool_conf) + self._launch(launch_request, tool_conf, adapter=adapter) - def test_res_link_launch_invalid_registration(self): - tool_conf, login_request, login_response = self._make_oidc_login() + @parameterized.expand([['django'], ['flask']]) + def test_res_link_launch_invalid_registration(self, adapter): + tool_conf, login_request, login_response = self._make_oidc_login(adapter=adapter) post_data = self.post_launch_data.copy() - launch_request = FakeRequest(post=post_data, - cookies=login_response.get_cookies_dict(), - session=login_request.session) + launch_request = self._get_request(login_request, login_response, post_data=post_data, adapter=adapter) # pylint: disable=deprecated-method with self.assertRaisesRegexp(LtiException, 'Client id not registered for this issuer'): - self._launch_with_invalid_jwt_body(self._get_data_with_invalid_aud, launch_request, tool_conf) + self._launch_with_invalid_jwt_body(self._get_data_with_invalid_aud, launch_request, tool_conf, + adapter=adapter) - def test_res_link_launch_invalid_deployment(self): - tool_conf, login_request, login_response = self._make_oidc_login() + @parameterized.expand([['django'], ['flask']]) + def test_res_link_launch_invalid_deployment(self, adapter): + tool_conf, login_request, login_response = self._make_oidc_login(adapter=adapter) post_data = self.post_launch_data.copy() - launch_request = FakeRequest(post=post_data, - cookies=login_response.get_cookies_dict(), - session=login_request.session) + launch_request = self._get_request(login_request, login_response, post_data=post_data, adapter=adapter) with self.assertRaisesRegexp(Exception, 'Unable to find deployment'): # pylint: disable=deprecated-method - self._launch_with_invalid_jwt_body(self._get_data_with_invalid_deployment, launch_request, tool_conf) + self._launch_with_invalid_jwt_body(self._get_data_with_invalid_deployment, launch_request, tool_conf, + adapter=adapter) - def test_res_link_launch_invalid_message(self): - tool_conf, login_request, login_response = self._make_oidc_login() + @parameterized.expand([['django'], ['flask']]) + def test_res_link_launch_invalid_message(self, adapter): + tool_conf, login_request, login_response = self._make_oidc_login(adapter=adapter) post_data = self.post_launch_data.copy() - launch_request = FakeRequest(post=post_data, - cookies=login_response.get_cookies_dict(), - session=login_request.session) + launch_request = self._get_request(login_request, login_response, post_data=post_data, adapter=adapter) with self.assertRaisesRegexp(LtiException, 'Incorrect version'): # pylint: disable=deprecated-method - self._launch_with_invalid_jwt_body(self._get_data_with_invalid_message, launch_request, tool_conf) + self._launch_with_invalid_jwt_body(self._get_data_with_invalid_message, launch_request, tool_conf, + adapter=adapter)