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)
Example #2
0
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)
Example #3
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'
Example #4
0
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'])
Example #5
0
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']
Example #6
0
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
Example #7
0
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
Example #8
0
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)
Example #9
0
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()]}
        )
Example #10
0
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 == '*****@*****.**'
Example #11
0
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,
            )
Example #12
0
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()}
        )
Example #13
0
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',
            }
        )
Example #14
0
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'
Example #15
0
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'
            },
        )
Example #16
0
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)
Example #17
0
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
Example #18
0
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
Example #19
0
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
Example #20
0
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()