diff --git a/tests/conftest.py b/tests/conftest.py index 568bef8881..16735468ab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,6 +26,7 @@ "h_api_url_private": "https://h.example.com/private/api/", "rpc_allowed_origins": ["http://localhost:5000"], "oauth2_state_secret": "test_oauth2_state_secret", + "onedrive_client_id": "test_one_drive_client_id", "session_cookie_secret": "notasecret", "via_secret": "not_a_secret", "blackboard_api_client_id": "test_blackboard_api_client_id", diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index e4a87fcec3..f1c6646371 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -1,9 +1,12 @@ import contextlib +import functools +import json +import os import re -from os import environ from urllib.parse import urlencode import httpretty +import oauthlib.oauth1 import pytest from _pytest.monkeypatch import MonkeyPatch from h_matchers import Any @@ -13,9 +16,10 @@ from lms import db from lms.app import create_app +from tests import factories from tests.conftest import TEST_SETTINGS -TEST_SETTINGS["database_url"] = environ["DATABASE_URL"] +TEST_SETTINGS["database_url"] = os.environ["DATABASE_URL"] TEST_ENVIRONMENT = { key.upper(): value for key, value in TEST_SETTINGS.items() if isinstance(value, str) @@ -25,6 +29,14 @@ ) +@pytest.fixture +def environ(): + environ = dict(os.environ) + environ.update(TEST_ENVIRONMENT) + + return environ + + @pytest.fixture(autouse=True) def clean_database(db_engine): """Delete any data added by the previous test.""" @@ -71,24 +83,52 @@ def db_session(db_engine, db_sessionfactory): connection.close() +def _lti_v11_launch(app, url, post_params, get_params=None, **kwargs): + if get_params: + url += f"?{urlencode(get_params)}" + + return app.post( + url, + params=post_params, + headers={ + "Accept": "text/html", + "Content-Type": "application/x-www-form-urlencoded", + }, + **kwargs, + ) + + @pytest.fixture def do_lti_launch(app): - def do_lti_launch(post_params, get_params=None, **kwargs): - url = "/lti_launches" - if get_params: - url += f"?{urlencode(get_params)}" - - return app.post( - url, - params=post_params, - headers={ - "Accept": "text/html", - "Content-Type": "application/x-www-form-urlencoded", - }, - **kwargs, - ) + return functools.partial(_lti_v11_launch, app, "/lti_launches") + + +@pytest.fixture +def do_deep_link_launch(app): + return functools.partial(_lti_v11_launch, app, "/content_item_selection") + + +@pytest.fixture +def get_client_config(): + def _get_client_config(response): + return json.loads(response.html.find("script", {"class": "js-config"}).string) + + return _get_client_config - return do_lti_launch + +@pytest.fixture +def application_instance(db_session): # noqa: ARG001 + return factories.ApplicationInstance( + tool_consumer_instance_guid="IMS Testing", + organization=factories.Organization(), + ) + + +@pytest.fixture +def oauth_client(application_instance): + return oauthlib.oauth1.Client( + application_instance.consumer_key, application_instance.shared_secret + ) @pytest.fixture(autouse=True) diff --git a/tests/functional/views/lti/basic_lti_launch_test.py b/tests/functional/views/lti/basic_lti_launch_test.py index 5d4561891f..903ebc12b1 100644 --- a/tests/functional/views/lti/basic_lti_launch_test.py +++ b/tests/functional/views/lti/basic_lti_launch_test.py @@ -1,4 +1,3 @@ -import json import time from urllib.parse import urlencode @@ -9,7 +8,6 @@ from lms.models import Assignment from lms.resources._js_config import JSConfig -from tests import factories class TestBasicLTILaunch: @@ -23,25 +21,27 @@ def test_requests_with_no_oauth_signature_are_forbidden( assert response.headers["Content-Type"] == Any.string.matching("^text/html") assert response.html - def test_unconfigured_basic_lti_launch(self, lti_params, do_lti_launch): + def test_unconfigured_basic_lti_launch( + self, lti_params, do_lti_launch, get_client_config + ): response = do_lti_launch( post_params=lti_params, status=200, ) - assert self.get_client_config(response)["mode"] == JSConfig.Mode.FILE_PICKER + assert get_client_config(response)["mode"] == JSConfig.Mode.FILE_PICKER def test_db_configured_basic_lti_launch( - self, lti_params, assignment, do_lti_launch + self, lti_params, assignment, do_lti_launch, get_client_config ): response = do_lti_launch(post_params=lti_params, status=200) - js_config = self.get_client_config(response) + js_config = get_client_config(response) assert js_config["mode"] == JSConfig.Mode.BASIC_LTI_LAUNCH assert urlencode({"url": assignment.document_url}) in js_config["viaUrl"] def test_basic_lti_launch_canvas_deep_linking_url( - self, do_lti_launch, url_launch_params, db_session + self, do_lti_launch, url_launch_params, db_session, get_client_config ): get_params, post_params = url_launch_params @@ -49,7 +49,7 @@ def test_basic_lti_launch_canvas_deep_linking_url( get_params=get_params, post_params=post_params, status=200 ) - js_config = self.get_client_config(response) + js_config = get_client_config(response) assert js_config["mode"] == JSConfig.Mode.BASIC_LTI_LAUNCH assert ( urlencode({"url": "https://url-configured.com/document.pdf"}) @@ -63,7 +63,7 @@ def test_basic_lti_launch_canvas_deep_linking_url( ) def test_basic_lti_launch_canvas_deep_linking_canvas_file( - self, do_lti_launch, db_session, canvas_file_launch_params + self, do_lti_launch, db_session, canvas_file_launch_params, get_client_config ): get_params, post_params = canvas_file_launch_params @@ -71,7 +71,7 @@ def test_basic_lti_launch_canvas_deep_linking_canvas_file( get_params=get_params, post_params=post_params, status=200 ) - js_config = self.get_client_config(response) + js_config = get_client_config(response) assert js_config["mode"] == JSConfig.Mode.BASIC_LTI_LAUNCH assert ( js_config["api"]["viaUrl"]["path"] @@ -84,13 +84,6 @@ def test_basic_lti_launch_canvas_deep_linking_canvas_file( == 1 ) - @pytest.fixture(autouse=True) - def application_instance(self, db_session): # noqa: ARG002 - return factories.ApplicationInstance( - tool_consumer_instance_guid="IMS Testing", - organization=factories.Organization(), - ) - @pytest.fixture def assignment(self, db_session, application_instance, lti_params): assignment = Assignment( @@ -104,12 +97,6 @@ def assignment(self, db_session, application_instance, lti_params): return assignment - @pytest.fixture - def oauth_client(self, application_instance): - return oauthlib.oauth1.Client( - application_instance.consumer_key, application_instance.shared_secret - ) - @pytest.fixture def lti_params(self, application_instance, sign_lti_params): params = { @@ -184,6 +171,3 @@ def _sign(params): return params return _sign - - def get_client_config(self, response): - return json.loads(response.html.find("script", {"class": "js-config"}).string) diff --git a/tests/functional/views/lti/deep_linking_test.py b/tests/functional/views/lti/deep_linking_test.py new file mode 100644 index 0000000000..0c6b5dec8c --- /dev/null +++ b/tests/functional/views/lti/deep_linking_test.py @@ -0,0 +1,122 @@ +import time + +import oauthlib.common +import oauthlib.oauth1 +import pytest + +from lms.resources._js_config import JSConfig + + +class TestDeepLinkingLaunch: + def test_basic_lti_launch_canvas_deep_linking_url( + self, + do_deep_link_launch, + lti_params, + application_instance, + get_client_config, + environ, + ): + response = do_deep_link_launch(post_params=lti_params, status=200) + + js_config = get_client_config(response) + assert js_config["mode"] == JSConfig.Mode.FILE_PICKER + assert js_config["filePicker"] == { + "autoGradingEnabled": True, + "blackboard": {"enabled": None}, + "canvas": { + "enabled": None, + "foldersEnabled": None, + "listFiles": { + "authUrl": "http://localhost/api/canvas/oauth/authorize", + "path": "/api/canvas/courses/None/files", + }, + "pagesEnabled": None, + }, + "canvasStudio": {"enabled": False}, + "d2l": {"enabled": False}, + "deepLinkingAPI": { + "data": { + "content_item_return_url": "https://apps.imsglobal.org/lti/cert/tp/tp_return.php/basic-lti-launch-request", + "context_id": "con-182", + "opaque_data_lti11": None, + }, + "path": "/lti/1.1/deep_linking/form_fields", + }, + "formAction": "https://apps.imsglobal.org/lti/cert/tp/tp_return.php/basic-lti-launch-request", + "formFields": { + "lti_message_type": "ContentItemSelection", + "lti_version": "LTI-1p0", + }, + "google": { + "clientId": environ["GOOGLE_CLIENT_ID"], + "developerKey": environ["GOOGLE_DEVELOPER_KEY"], + "enabled": True, + "origin": application_instance.lms_url, + }, + "jstor": {"enabled": False}, + "ltiLaunchUrl": "http://localhost/lti_launches", + "microsoftOneDrive": { + "clientId": environ["ONEDRIVE_CLIENT_ID"], + "enabled": True, + "redirectURI": "http://localhost/onedrive/filepicker/redirect", + }, + "moodle": {"enabled": None, "pagesEnabled": None}, + "promptForTitle": True, + "vitalSource": {"enabled": False}, + "youtube": {"enabled": True}, + } + + +@pytest.fixture +def lti_params(application_instance, sign_lti_params): + params = { + "context_id": "con-182", + "context_label": "SI182", + "context_title": "Design of Personal Environments", + "context_type": "CourseSection", + "custom_context_memberships_url": "https://apps.imsglobal.org/lti/cert/tp/tp_membership.php/context/con-182/membership?b64=a2puNjk3b3E5YTQ3Z28wZDRnbW5xYzZyYjU%3D", + "custom_context_setting_url": "https://apps.imsglobal.org/lti/cert/tp/tp_settings.php/lis/CourseSection/con-182/bindings/ims/cert/custom?b64=a2puNjk3b3E5YTQ3Z28wZDRnbW5xYzZyYjU%3D", + "custom_link_setting_url": "$LtiLink.custom.url", + "custom_system_setting_url": "https://apps.imsglobal.org/lti/cert/tp/tp_settings.php/ToolProxy/Hypothesis1b40eafba184a131307049e01e9c147d/custom?b64=a2puNjk3b3E5YTQ3Z28wZDRnbW5xYzZyYjU%3D", + "custom_tc_profile_url": "https://apps.imsglobal.org/lti/cert/tp/tp_tcprofile.php?b64=a2puNjk3b3E5YTQ3Z28wZDRnbW5xYzZyYjU%3D", + "launch_presentation_document_target": "iframe", + "launch_presentation_locale": "en_US", + "launch_presentation_return_url": "https://apps.imsglobal.org/lti/cert/tp/tp_return.php/basic-lti-launch-request", + "lis_course_section_sourcedid": "id-182", + "lis_person_contact_email_primary": "jane@school.edu", + "lis_person_name_family": "Lastname", + "lis_person_name_full": "Jane Q. Lastname", + "lis_person_name_given": "Jane", + "lis_person_sourcedid": "school.edu:jane", + "lti_message_type": "ContentItemSelectionRequest", + "lti_version": "LTI-1p0", + "oauth_callback": "about:blank", + "oauth_consumer_key": application_instance.consumer_key, + "oauth_nonce": "38d6db30e395417659d068164ca95169", + "oauth_signature_method": "HMAC-SHA1", + "oauth_timestamp": str(int(time.time())), + "oauth_version": "1.0", + "roles": "Instructor", + "tool_consumer_info_product_family_code": "imsglc", + "tool_consumer_info_version": "1.1", + "tool_consumer_instance_description": "IMS Testing Description", + "tool_consumer_instance_guid": application_instance.tool_consumer_instance_guid, + "tool_consumer_instance_name": "IMS Testing Instance", + "user_id": "123456", + "content_item_return_url": "https://apps.imsglobal.org/lti/cert/tp/tp_return.php/basic-lti-launch-request", + } + + return sign_lti_params(params) + + +@pytest.fixture +def sign_lti_params(oauth_client): + def _sign(params): + params["oauth_signature"] = oauth_client.get_oauth_signature( + oauthlib.common.Request( + "http://localhost/content_item_selection", "POST", body=params + ) + ) + return params + + return _sign