Skip to content

Commit

Permalink
Basic functional test for DeepLinking launches
Browse files Browse the repository at this point in the history
  • Loading branch information
marcospri committed Dec 13, 2024
1 parent 2f58d35 commit dc7db58
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 43 deletions.
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
74 changes: 57 additions & 17 deletions tests/functional/conftest.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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."""
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 10 additions & 26 deletions tests/functional/views/lti/basic_lti_launch_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import time
from urllib.parse import urlencode

Expand All @@ -9,7 +8,6 @@

from lms.models import Assignment
from lms.resources._js_config import JSConfig
from tests import factories


class TestBasicLTILaunch:
Expand All @@ -23,33 +21,35 @@ 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

response = do_lti_launch(
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"})
Expand All @@ -63,15 +63,15 @@ 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

response = do_lti_launch(
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"]
Expand All @@ -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(
Expand All @@ -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 = {
Expand Down Expand Up @@ -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)
122 changes: 122 additions & 0 deletions tests/functional/views/lti/deep_linking_test.py
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"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

0 comments on commit dc7db58

Please sign in to comment.