def test_get_launch_url(describe, logged_in, admin_user, test_client): with describe('setup'), logged_in(admin_user): canvas_prov = helpers.to_db_object( helpers.create_lti1p3_provider(test_client, 'Canvas'), m.LTI1p3Provider ) moodle_prov = helpers.to_db_object( helpers.create_lti1p3_provider(test_client, 'Moodle'), m.LTI1p3Provider ) with describe('Should return a furl object'): # Furl is not typed yet so it makes sense to check this as mypy sees it # as an `Any` url = canvas_prov.get_launch_url(goto_latest_sub=False) assert isinstance(url, furl.furl) with describe('should not include an ID for canvas'): url = canvas_prov.get_launch_url(goto_latest_sub=False) assert canvas_prov.id not in str(url) with describe('should not include an ID for moodle'): url = moodle_prov.get_launch_url(goto_latest_sub=False) assert moodle_prov.id in str(url) with describe('should be possible to launch to the latest submission'): url = canvas_prov.get_launch_url(goto_latest_sub=True) assert '/launch_to_latest_submission' in str(url)
def test_get_providers( test_client, describe, logged_in, admin_user, teacher_user, watch_signal, lms, iss ): with describe('pre-check'), logged_in(admin_user): test_client.req( 'get', '/api/v1/lti1.3/providers/', 200, result=[], ) with describe('setup'), logged_in(admin_user): provider = helpers.create_lti1p3_provider( test_client, lms, iss=iss, client_id=str(uuid.uuid4()) + '_lms=' + lms, ) assig_created = watch_signal(signals.ASSIGNMENT_CREATED) with describe('should get registered providers'), logged_in(admin_user): test_client.req( 'get', '/api/v1/lti1.3/providers/', 200, result=[provider], ) with describe( 'should be possible to add multiple providers for the same LMS' ), logged_in(admin_user): provider2 = helpers.create_lti1p3_provider( test_client, lms, iss=iss, client_id=str(uuid.uuid4()) + '_lms=' + lms, ) with describe('should get registered providers'), logged_in(admin_user): test_client.req( 'get', '/api/v1/lti1.3/providers/', 200, result=[provider, provider2], ) with describe('should not be visible to non-admin users' ), logged_in(teacher_user): test_client.req( 'get', '/api/v1/lti1.3/providers/', 200, result=[], ) assert assig_created.was_send_n_times(0)
def test_with_wrong_nonce(test_client, describe, logged_in, admin_user, monkeypatched_passback): with describe('setup'), logged_in(admin_user): lti_assig_id = str(uuid.uuid4()) lti_course_id = str(uuid.uuid4()) lms = 'Canvas' provider = helpers.create_lti1p3_provider( test_client, lms, iss='https://canvas.instructure.com', client_id=str(uuid.uuid4()) + '_lms=' + lms) with describe('can do oidc launch with GET request'), test_client as c: oidc = do_oidc_login(c, provider) with c.session_transaction() as sess: s_service = lti1p3.flask.FlaskSessionService( lti1p3.flask.FlaskRequest(force_post=False)) sess[s_service._get_key('lti-nonce')] = 'Not correct' response = do_lti_launch( c, provider, make_launch_data( CANVAS_DATA, provider, { 'Assignment.id': lti_assig_id, 'Course.id': lti_course_id, }, ), oidc, 400, ) assert response['message'] == 'Invalid Nonce'
def test_launch_redirect_to_given_page(test_client, describe, logged_in, admin_user, app): with describe('setup'), logged_in(admin_user): red_url = f'https://{uuid.uuid4()}.com' provider = helpers.create_lti1p3_provider( test_client, 'Canvas', iss='https://canvas.instructure.com', client_id=str(uuid.uuid4()) + '_lms=' + 'Canvas') data = make_launch_data( CANVAS_DATA, provider, { 'Assignment.id': 'assig id', 'Course.id': 'asdfs', }, ) url = furl.furl('/api/v1/lti1.3/launch').add({ 'platform_redirect_url': red_url, 'full_win_launch_requested': '1', }).tostr() oidc = do_oidc_login(test_client, provider) oidc_params = oidc.query.params with describe('should redirect to url given in launch request'): made_jwt = jwt.encode( { **data, 'nonce': oidc_params['nonce'] }, LTI_JWT_SECRET, algorithm='HS256', ) response = test_client.post( url, data={ 'id_token': made_jwt, 'state': oidc_params['state'], }, ) assert response.status_code == 302 # Should redirect to the given get parameter url assert response.headers['Location'] == red_url with describe('should check jwt before redirecting'): oidc = do_oidc_login(test_client, provider) response = test_client.post( url, data={ 'id_token': 'NOT_VALID_JWT', 'state': oidc_params['state'], }, ) assert response.status_code == 303 assert response.headers['Location'] != red_url assert response.headers['Location'].startswith( app.config['EXTERNAL_URL'])
def test_error_when_launching_without_resource_id(test_client, describe, logged_in, admin_user, stub_function): with describe('setup'), logged_in(admin_user): # Make sure our own validation works, so don't use the one of pytli1p3 stub_function(pylti1p3.message_validators.ResourceMessageValidator, 'validate', lambda: True) provider = helpers.create_lti1p3_provider( test_client, 'Canvas', iss='https://canvas.instructure.com', client_id=str(uuid.uuid4()) + '_lms=' + 'Canvas') data = make_launch_data( CANVAS_DATA, provider, { 'Assignment.id': None, 'Course.id': 'asdfs', }, ) with describe('should fail without email'): _, err = do_oidc_and_lti_launch(test_client, provider, data, 400) assert 'No resource id was provided for this launch' in err['message']
def lti1p3_provider(logged_in, admin_user, test_client): with logged_in(admin_user): prov = helpers.to_db_object( helpers.create_lti1p3_provider(test_client, 'Canvas'), m.LTI1p3Provider ) yield prov
def test_copying_email_from_launch(test_client, describe, logged_in, admin_user, stub_function, monkeypatched_passback, session): with describe('setup'), logged_in(admin_user): email1 = str(uuid.uuid4()) email2 = str(uuid.uuid4()) provider = helpers.create_lti1p3_provider( test_client, 'Canvas', iss='https://canvas.instructure.com', client_id=str(uuid.uuid4()) + '_lms=' + 'Canvas') lti_user_id = str(uuid.uuid4()) data = make_launch_data( CANVAS_DATA, provider, { 'Assignment.id': 'assig id', 'Course.id': 'asdfs', 'User.id': lti_user_id, }, ) with describe('should create a user with the given email'): do_oidc_and_lti_launch(test_client, provider, merge(data, {'email': email1}), 200) user = m.UserLTIProvider.query.filter_by( lti_user_id=lti_user_id).one().user assert user.email == email1 with describe('should not always copy the new email'): _, launch = do_oidc_and_lti_launch(test_client, provider, merge(data, {'email': email2}), 200) user2 = m.UserLTIProvider.query.filter_by( lti_user_id=lti_user_id).one().user assert user2 == user assert user2.email == email1 assert launch['data']['updated_email'] is None with describe('should copy the email if instructed'): user.reset_email_on_lti = True session.commit() _, launch = do_oidc_and_lti_launch(test_client, provider, merge(data, {'email': email2}), 200) user3 = m.UserLTIProvider.query.filter_by( lti_user_id=lti_user_id).one().user assert user2 == user assert user3.email == email2 assert not user3.reset_email_on_lti assert launch['data']['updated_email'] == email2
def test_error_when_no_cookies(test_client, describe, logged_in, admin_user): with describe('setup'), logged_in(admin_user): lti_assig_id = str(uuid.uuid4()) lti_course_id = str(uuid.uuid4()) lms = 'Canvas' provider = helpers.create_lti1p3_provider( test_client, lms, iss='https://canvas.instructure.com', client_id=str(uuid.uuid4()) + '_lms=' + lms) data = make_launch_data(CANVAS_DATA, provider, { 'Assignment.id': lti_assig_id, 'Course.id': lti_course_id }) def assert_is_cookie_error(response): assert response['message'] == "Couldn't set needed cookies" assert response['code'] == 'LTI1_3_ERROR' assert response['original_exception'][ 'code'] == 'LTI1_3_COOKIE_ERROR' with describe('error when no cookies are present at all'): oidc = do_oidc_login(test_client, provider) test_client.cookie_jar.clear() response = do_lti_launch(test_client, provider, data, oidc, 400) assert_is_cookie_error(response) with describe('error when old launch cookies are present'): oidc = do_oidc_login(test_client, provider) old_jar = copy.copy(test_client.cookie_jar) test_client.cookie_jar.clear() with freezegun.freeze_time(DatetimeWithTimezone.utcnow() + timedelta(hours=1)): oidc = do_oidc_login(test_client, provider) test_client.cookie_jar = old_jar response = do_lti_launch(test_client, provider, data, oidc, 400) assert_is_cookie_error(response) with describe('error when cookie has bogus value'): oidc = do_oidc_login(test_client, provider) all_cookies = list(test_client.cookie_jar) test_client.cookie_jar.clear() for cook in all_cookies: cook.value = 'HAHA BOGUS VALUE' test_client.cookie_jar.set_cookie(cook) response = do_lti_launch(test_client, provider, data, oidc, 400) assert_is_cookie_error(response)
def test_get_jwks_for_provider( test_client, describe, lms, logged_in, admin_user ): with describe('setup'), logged_in(admin_user): prov = helpers.to_db_object( helpers.create_lti1p3_provider(test_client, lms), m.LTI1p3Provider ) url = f'/api/v1/lti1.3/providers/{helpers.get_id(prov)}/jwks' with describe( 'should be possible to get json config without being logged in' ): test_client.req( 'get', url, 200, result={'keys': [prov.get_public_jwk()]} )
def test_launch_with_missing_data(test_client, describe, logged_in, admin_user, watch_signal): with describe('setup'), logged_in(admin_user): user_added = watch_signal(signals.USER_ADDED_TO_COURSE, clear_all_but=[]) provider = helpers.create_lti1p3_provider( test_client, 'Canvas', iss='https://canvas.instructure.com', client_id=str(uuid.uuid4()) + '_lms=' + 'Canvas') data = make_launch_data( CANVAS_DATA, provider, { 'Assignment.id': 'asdf', 'Course.id': 'asdfs', }, ) def ensure_fails(data, err_msg): _, launch = do_oidc_and_lti_launch(test_client, provider, data, 400) assert err_msg in launch['message'] assert user_added.was_not_send return launch with describe('should fail without email'): ensure_fails(remove_from(data, 'email'), 'requires the email of the user') with describe('should fail without nrps'): ensure_fails(remove_from(data, lti1p3.claims.NAMESROLES), 'NamesRoles Provisioning service is not enabled') with describe('should fail without custom args'): ensure_fails(remove_from(data, lti1p3.claims.CUSTOM), 'The LTI launch is missing required custom claims') with describe('not fail without email for test student'): do_oidc_and_lti_launch( test_client, provider, remove_from(merge(data, {'name': 'Test Student'}), 'email'), ) assert user_added.was_send_once assert user_added.signal_arg.user.email == '*****@*****.**'
def test_launch_with_incorrect_provider(test_client, describe, logged_in, admin_user, stub_function, monkeypatched_passback, session, app): with describe('setup'), logged_in(admin_user): # Make sure our own validation works, so don't use the one of pytli1p3 stub_function(pylti1p3.message_validators.ResourceMessageValidator, 'validate', lambda: True) provider = helpers.to_db_object( helpers.create_lti1p3_provider( test_client, 'Canvas', iss='https://canvas.instructure.com', client_id=str(uuid.uuid4()) + '_lms=' + 'Canvas'), m.LTI1p3Provider) def assert_launch_errors(msg, override, with_id=False): oidc = do_oidc_login( test_client, provider, redirect_to=app.config['EXTERNAL_URL'], override_data=override, with_id=with_id, ) blob_id = oidc.query.params['blob_id'] err = test_client.req('post', '/api/v1/lti/launch/2?extended', 400, data={'blob_id': blob_id}) assert msg in err['message'] return err with describe('cannot launch with non finalized provider'): provider._finalized = False session.commit() assert_launch_errors('This LTI connection is not yet finalized', {}) provider._finalized = True session.commit() with describe('cannot launch with incorrect iss'): for with_id in [True, False]: assert_launch_errors( 'This LMS was not found as a LTIProvider', {'iss': 'OTHER ISS'}, with_id=with_id, )
def test_setting_deadline_for_assignment( test_client, describe, logged_in, admin_user, session, tomorrow, lms, err_code ): with describe('setup'), logged_in(admin_user): prov = helpers.to_db_object( helpers.create_lti1p3_provider(test_client, lms), m.LTI1p3Provider ) course, _ = helpers.create_lti1p3_course(test_client, session, prov) assig = helpers.create_lti1p3_assignment(session, course) with describe('should maybe be possible to update the deadline' ), logged_in(admin_user): test_client.req( 'patch', f'/api/v1/assignments/{helpers.get_id(assig)}', err_code, data={'deadline': tomorrow.isoformat()} )
def test_get_json_config_for_provider( test_client, describe, lms, logged_in, admin_user, app, monkeypatch ): with describe('setup'), logged_in(admin_user): prov = helpers.create_lti1p3_provider(test_client, lms) url = f'/api/v1/lti1.3/providers/{helpers.get_id(prov)}/config' ext_url = f'{uuid.uuid4()}.com' monkeypatch.setitem(app.config, 'EXTERNAL_URL', ext_url) with describe( 'should be possible to get json config without being logged in' ): test_client.req( 'get', url, 200, result={ '__allow_extra__': True, 'target_link_uri': f'{ext_url}/api/v1/lti1.3/launch', 'oidc_initiation_url': f'{ext_url}/api/v1/lti1.3/login', } )
def test_launch_to_latest_submission(test_client, describe, logged_in, admin_user): with describe('setup'), logged_in(admin_user): provider = helpers.create_lti1p3_provider( test_client, 'Canvas', iss='https://canvas.instructure.com', client_id=str(uuid.uuid4()) + '_lms=' + 'Canvas') data = make_launch_data( CANVAS_DATA, provider, { 'Assignment.id': 'assig id', 'Course.id': 'asdfs', }, ) with describe('incorrect oidc login shows a nice error page'): oidc = do_oidc_login(test_client, provider) oidc_params = oidc.query.params url = '/api/v1/lti1.3/launch_to_latest_submission' made_jwt = jwt.encode( { **data, 'nonce': oidc_params['nonce'] }, LTI_JWT_SECRET, algorithm='HS256', ) response = test_client.post( url, data={ 'id_token': made_jwt, 'state': oidc_params['state'], }, ) assert response.status_code == 303 url = furl.furl(response.headers['Location']) assert url.query.params['goto_latest_submission'] == 'True'
def test_wrong_launch_to_oidc_login(test_client, describe, session, app, logged_in, admin_user): with describe('setup'), logged_in(admin_user): provider = helpers.create_lti1p3_provider(test_client, 'Canvas', iss='other iss', client_id=str(uuid.uuid4()) + '_lms=' + 'Canvas') with describe('incorrect oidc login shows a nice error page'): oidc = do_oidc_login(test_client, provider, redirect_to=app.config['EXTERNAL_URL'], override_data={'iss': None}) blob_id = oidc.query.params['blob_id'] test_client.req( 'post', '/api/v1/lti/launch/2?extended', 400, data={'blob_id': blob_id}, result={ '__allow_extra__': True, 'message': 'Could not find issuer' }, )
def test_direct_deep_link_launch(test_client, describe, logged_in, admin_user): with describe('setup'), logged_in(admin_user): canvas_provider = helpers.create_lti1p3_provider( test_client, 'Canvas', iss='https://canvas.instructure.com', client_id=str(uuid.uuid4()) + '_lms=' + 'Canvas') return_url = f'https://{uuid.uuid4()}.com' deep_link_data = { lti1p3.claims.MESSAGE_TYPE: 'LtiDeepLinkingRequest', lti1p3.claims.DEEP_LINK: { 'data': 'DP_DATA', 'deep_link_return_url': return_url, 'accept_types': ['ltiResourceLink'], 'accept_presentation_document_targets': 'PRES', }, } canvas_launch_data = make_launch_data( merge(CANVAS_DATA, deep_link_data), canvas_provider, {}) with describe('deep link to canvas should directly return resource'): _, launch = do_oidc_and_lti_launch(test_client, canvas_provider, canvas_launch_data, status=DEEP_LINK) form = launch.data parsed = defused_xml_fromstring(form.decode('utf8')) assert parsed.tag == 'html' form, = parsed.findall('body/form') assert form.attrib['action'] == return_url assert form.attrib['method'] == 'POST' child, = form.getchildren() assert child.attrib['name'] == 'JWT' assert child.attrib['type'] == 'hidden' jwt_value = child.attrib['value'] assert jwt.decode(jwt_value, verify=False)
def test_validate_jwt_body(test_client, describe, logged_in, admin_user, stub_function, session, app, monkeypatched_validate_jwt, monkeypatched_passback): with describe('setup'), logged_in(admin_user): stub_err = lambda: False super_decode = jwt.decode def maybe_err(*args, **kwargs): if stub_err() and not flask.request.path.endswith('launch/2'): raise jwt.InvalidTokenError('CG TEST ERROR') return super_decode(*args, **kwargs) stub_decode = stub_function(jwt, 'decode', maybe_err, with_args=True) key = jwcrypto.jwk.JWK.generate(kty='RSA') kid = str(uuid.uuid4()) public_key = { 'keys': [{ **json.loads(key.export_public()), 'kid': kid }], } stub_fetch = stub_function(pylti1p3.message_launch.MessageLaunch, 'fetch_public_key', lambda: public_key) provider = helpers.create_lti1p3_provider( test_client, 'Canvas', iss='https://canvas.instructure.com', client_id=str(uuid.uuid4()) + '_lms=' + 'Canvas') data = make_launch_data(CANVAS_DATA, provider, { 'Assignment.id': 'a_id', 'Course.id': 'c_id' }) def make_jwt(data, oidc_params): return jwt.encode( { **data, 'nonce': oidc_params['nonce'] }, key.export_to_pem(private_key=True, password=None), algorithm='RS256', headers={'kid': kid}, ) def do_launch(status): oidc = do_oidc_login(test_client, provider) return do_lti_launch(test_client, provider, data, oidc, status, make_jwt=make_jwt) with describe('should not error when stub_decode does not error'): stub_err = lambda: False do_launch(200) assert stub_fetch.called_amount == 1 with describe('should error when stub_decodes errors'): # Flush all cache so fetch count is correct app.inter_request_cache.lti_access_tokens._redis.flushall() stub_err = lambda: True err = do_launch(400) assert err['message'] == "Can't decode id_token: CG TEST ERROR" # Twice, as it should refresh the token after an error assert stub_fetch.called_amount == 2 with describe('failing once should result in a successful launch'): # Flush all cache so fetch count is correct app.inter_request_cache.lti_access_tokens._redis.flushall() amount = 1 def _err_once(): nonlocal amount if amount > 0: amount -= 1 return True return False stub_err = _err_once do_launch(200) assert stub_fetch.called_amount == 2
def test_do_simple_launch(test_client, describe, logged_in, admin_user, watch_signal, launch_data, lms, iss, monkeypatched_validate_jwt, monkeypatched_passback, yesterday, tomorrow): with describe('setup'), logged_in(admin_user): provider = helpers.create_lti1p3_provider(test_client, lms, iss=iss, client_id=str(uuid.uuid4()) + '_lms=' + lms) user_added = watch_signal(signals.USER_ADDED_TO_COURSE) assig_created = watch_signal(signals.ASSIGNMENT_CREATED) lti_user_id = str(uuid.uuid4()) lti_assig_id = str(uuid.uuid4()) lti_assig_id2 = str(uuid.uuid4()) lti_course_id = str(uuid.uuid4()) deadline = DatetimeWithTimezone.utcnow() + timedelta( days=randint(3, 10)) with describe('Initial launch creates assignment and course'): _, launch_result = do_oidc_and_lti_launch( test_client, provider, make_launch_data( launch_data, provider, { 'Assignment.id': lti_assig_id, 'Course.id': lti_course_id, 'cg_deadline': deadline.isoformat(), 'cg_available_at': tomorrow.isoformat(), 'User.id': lti_user_id, }, ), ) assert monkeypatched_validate_jwt.called_amount == 1 assert user_added.was_send_once assert monkeypatched_passback.called_amount == 1 passback_arg = monkeypatched_passback.all_args[0] assert len(passback_arg) == 1 assert passback_arg[0].get_score_given() is None assert passback_arg[0].get_activity_progress() == 'Initialized' assert assig_created.was_send_once created_assig = m.Assignment.query.get(assig_created.signal_arg.id) assert created_assig.lti_assignment_id == lti_assig_id assert created_assig.deadline == deadline assert launch_result['data']['assignment']['state'] == 'hidden' assert launch_result['version'] == 'v1_3' assert launch_result['data']['type'] == 'normal_result' course = launch_result['data']['course'] assig = launch_result['data']['assignment'] assert assig['id'] == assig_created.signal_arg.id assert course['is_lti'] with describe('Second launch does not create assignment again'): new_deadline = deadline + timedelta(days=1) _, launch_result2 = do_oidc_and_lti_launch( test_client, provider, make_launch_data( launch_data, provider, { 'Course.id': lti_course_id, 'Assignment.id': lti_assig_id, 'cg_deadline': new_deadline.isoformat(), 'cg_available_at': yesterday.isoformat(), 'User.id': lti_user_id, }, ), ) assert monkeypatched_validate_jwt.called_amount == 1 assert user_added.was_not_send assert assig_created.was_not_send assert launch_result2['data']['course']['id'] == course['id'] assert launch_result2['data']['assignment']['id'] == assig['id'] # Deadline should be updated assert launch_result2['data']['assignment'][ 'deadline'] == new_deadline.isoformat() assert launch_result2['data']['assignment']['state'] == 'submitting' with describe('New launch in same course only creates assignment'): _, launch_result3 = do_oidc_and_lti_launch( test_client, provider, make_launch_data( launch_data, provider, { 'Course.id': lti_course_id, 'Assignment.id': lti_assig_id2, 'User.id': lti_user_id, }, ), ) assert monkeypatched_validate_jwt.called_amount == 1 assert user_added.was_not_send assert assig_created.was_send_once assert launch_result3['data']['course']['id'] == course['id'] assert launch_result3['data']['assignment']['id'] != assig['id'] # Deadline was not passed so it should not be set assert launch_result3['data']['assignment']['deadline'] is None
def test_connecting_users_and_courses_lti1p1_provider( test_client, describe, logged_in, admin_user, stub_function, monkeypatched_passback, session, connect, canvas_lti1p1_provider, watch_signal, add_user_data, add_course_data, add_assig_data): with describe('setup'), logged_in(admin_user): watch_signal(signals.USER_ADDED_TO_COURSE, clear_all_but=[]) old_lti_context_id = str(uuid.uuid4()) new_lti_context_id = str(uuid.uuid4()) old_resource_id = str(uuid.uuid4()) new_resource_id = str(uuid.uuid4()) original_user = helpers.create_user_with_role(session, 'Student', []) session.commit() old_lti_user_id = str(uuid.uuid4()) assert original_user session.add( m.UserLTIProvider(user=original_user, lti_provider=canvas_lti1p1_provider, lti_user_id=old_lti_user_id)) course = m.Course.create_and_add(name='LTI COURSE JEE') m.CourseLTIProvider.create_and_add( course=course, lti_provider=canvas_lti1p1_provider, lti_context_id=old_lti_context_id, deployment_id=old_lti_context_id, ) assig = m.Assignment(course=course, name='Name', is_lti=True, lti_assignment_id=old_resource_id) session.add(assig) session.commit() provider = helpers.create_lti1p3_provider( test_client, 'Canvas', iss='https://canvas.instructure.com', client_id=str(uuid.uuid4()) + '_lms=' + 'Canvas') provider = helpers.to_db_object(provider, m.LTI1p3Provider) if connect: provider._updates_lti1p1 = canvas_lti1p1_provider session.commit() lti_user_id = str(uuid.uuid4()) data = make_launch_data( CANVAS_DATA, provider, { 'Assignment.id': new_resource_id, 'Course.id': new_lti_context_id, 'User.id': lti_user_id, }, ) to_merge = {} if add_course_data: to_merge['context_id'] = old_lti_context_id if add_user_data: to_merge['user_id'] = old_lti_user_id if add_assig_data: to_merge['resource_link_id'] = old_resource_id extra_data = { "https://purl.imsglobal.org/spec/lti/claim/lti1p1": to_merge, } with describe('should create a user with the given email'): complete_data = merge(data, extra_data) _, launch = do_oidc_and_lti_launch(test_client, provider, complete_data, 200) new_user = m.UserLTIProvider.query.filter_by( lti_user_id=lti_user_id).one().user if connect and add_user_data: assert new_user.id == original_user.id else: assert new_user.id != original_user.id if connect and add_course_data: assert launch['data']['course']['id'] == course.id else: assert launch['data']['course']['id'] != course.id if connect and add_assig_data and add_course_data: assert launch['data']['assignment']['id'] == assig.id assert m.Assignment.query.get( assig.id).lti_assignment_id == new_resource_id else: assert launch['data']['assignment']['id'] != assig.id
def test_real_deep_link_launch(test_client, describe, logged_in, admin_user): with describe('setup'), logged_in(admin_user): brightspace_provider = helpers.create_lti1p3_provider( test_client, 'Brightspace', iss='https://partners.brightspace.com', client_id=str(uuid.uuid4()) + '_lms=' + 'Lichtruimte') return_url = f'https://{uuid.uuid4()}.com' deep_link_data = { lti1p3.claims.MESSAGE_TYPE: 'LtiDeepLinkingRequest', lti1p3.claims.DEEP_LINK: { 'data': 'DP_DATA', 'deep_link_return_url': return_url, 'accept_types': ['ltiResourceLink'], 'accept_presentation_document_targets': 'PRES', }, } launch_data = make_launch_data(merge(BRIGHTSPACE_DATA, deep_link_data), brightspace_provider, {}) def get_new_deep_link(): _, launch = do_oidc_and_lti_launch(test_client, brightspace_provider, launch_data) assert launch['data']['type'] == 'deep_link' blob_id = launch['data']['deep_link_blob_id'] auth_token = launch['data']['auth_token'] return auth_token, f'/api/v1/lti1.3/deep_link/{blob_id}' with describe('have to provide correct auth token to use blob id'): auth_token, url = get_new_deep_link() res = test_client.req('post', url, 403, data={ 'auth_token': f'WRONG_{auth_token}', 'name': 'assig_name', 'deadline': 'NOT A DEADLINE', }, result={ '__allow_extra__': True, 'message': regex('not provide the correct token'), }) with describe('too old blob id is also rejected'): auth_token, url = get_new_deep_link() with freezegun.freeze_time(DatetimeWithTimezone.utcnow() + timedelta(days=2)): test_client.req( 'post', url, 400, result={ '__allow_extra__': True, 'message': regex('^This deep linking session has expired'), }) with describe('returns deep link form if all is correct'): auth_token, url = get_new_deep_link() deadline = DatetimeWithTimezone.utcnow() + timedelta(days=4) res = test_client.req('post', url, 200, data={ 'auth_token': auth_token, 'name': 'assig_name', 'deadline': deadline.isoformat(), }, result={ 'jwt': str, 'url': return_url, }) jwt_parsed = jwt.decode(res['jwt'], verify=False) content_items = jwt_parsed[ 'https://purl.imsglobal.org/spec/lti-dl/claim/content_items'] assert isinstance(content_items, list) assert len(content_items) == 1 assert content_items[0]['title'] == 'assig_name' assert content_items[0]['submission'][ 'endDateTime'] == deadline.isoformat()