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_getting_peer_feedback_connections(test_client, admin_user, session,
                                           describe, logged_in, yesterday,
                                           tomorrow):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assignment = helpers.create_assignment(test_client,
                                               course,
                                               deadline=yesterday,
                                               state='open')
        teacher = helpers.create_user_with_role(session, 'Teacher', course)
        users = [
            helpers.get_id(
                helpers.create_user_with_role(session, 'Student', course))
            for _ in range(5)
        ]
        user1, user2, *_ = users
        for user in users:
            helpers.create_submission(test_client, assignment, for_user=user)

        helpers.enable_peer_feedback(test_client, assignment, amount=1)
        url = (f'/api/v1/assignments/{helpers.get_id(assignment)}/users'
               f'/{helpers.get_id(user1)}/peer_feedback_subjects/')
        conns = get_all_connections(assignment, 1)[user1]

    with describe('Can get connections for own user'):
        with logged_in(user1):
            api_conns = test_client.req(
                'get',
                url,
                200,
                result=[{
                    'peer': helpers.to_db_object(user1, m.User),
                    'subject': helpers.to_db_object(conns[0], m.User),
                }])

    with describe('Cannot get connections for other user as student'):
        with logged_in(user2):
            test_client.req('get', url, 403)

    with describe('Can get connections for other user as teacher'):
        with logged_in(teacher):
            test_client.req('get', url, 200, result=api_conns)

    with describe(
            'No connections if assignments deadline has not expired just yet'):
        with logged_in(teacher):
            test_client.req(
                'patch',
                f'/api/v1/assignments/{helpers.get_id(assignment)}',
                200,
                data={'deadline': tomorrow.isoformat()})

        with logged_in(user1):
            test_client.req('get', url, 200, result=[])

        # Not even as teacher
        with logged_in(teacher):
            test_client.req('get', url, 200, result=[])
def test_delete_submission_of_group(
    lti1p3_provider, describe, logged_in, admin_user, watch_signal,
    stub_function, test_client, session, tomorrow
):
    with describe('setup'), logged_in(admin_user):
        watch_signal(signals.WORK_CREATED, clear_all_but=[])
        watch_signal(signals.GRADE_UPDATED, clear_all_but=[])
        watch_signal(signals.USER_ADDED_TO_COURSE, clear_all_but=[])
        stub_function(
            pylti1p3.service_connector.ServiceConnector,
            'get_access_token', lambda: ''
        )
        stub_passback = stub_function(
            pylti1p3.assignments_grades.AssignmentsGradesService, 'put_grade'
        )

        course, course_conn = helpers.create_lti1p3_course(
            test_client, session, lti1p3_provider
        )
        assig = helpers.create_lti1p3_assignment(
            session, course, state='done', deadline=tomorrow
        )

        # Create some users and let them have individual submissions
        g_user1 = helpers.create_lti1p3_user(session, lti1p3_provider)
        g_user2 = helpers.create_lti1p3_user(session, lti1p3_provider)
        for user in [g_user1, g_user2]:
            course_conn.maybe_add_user_to_course(user, ['Learner'])
            sub = helpers.create_submission(test_client, assig, for_user=user)
            helpers.to_db_object(sub, m.Work).set_grade(
                5.0, m.User.resolve(admin_user)
            )

        # Place the users in a group, with a submission with a different grade
        # than their individual submission.
        gset = helpers.create_group_set(test_client, course, 1, 2, [assig])
        helpers.create_group(test_client, gset, [g_user1, g_user2])
        gsub = helpers.create_submission(test_client, assig, for_user=g_user2)
        helpers.to_db_object(gsub, m.Work).set_grade(
            6.0, m.User.resolve(admin_user)
        )

        session.commit()

    with describe(
        'deleting group submission passes back individual submissions'
    ):
        with logged_in(admin_user):
            test_client.req(
                'delete', f'/api/v1/submissions/{helpers.get_id(gsub)}', 204
            )

        assert stub_passback.called_amount == 2

        # The grade passed back is that of their individual submission
        assert stub_passback.args[0][0].get_score_given() == 5.0
        assert stub_passback.args[1][0].get_score_given() == 5.0
def test_passback_single_submission(
    lti1p3_provider, describe, logged_in, admin_user, watch_signal,
    stub_function, test_client, session, tomorrow
):
    with describe('setup'), logged_in(admin_user):
        watch_signal(signals.WORK_CREATED, clear_all_but=[])
        watch_signal(signals.USER_ADDED_TO_COURSE, clear_all_but=[])
        signal = watch_signal(
            signals.GRADE_UPDATED,
            clear_all_but=[m.LTI1p3Provider._passback_submission]
        )

        stub_function(
            pylti1p3.service_connector.ServiceConnector,
            'get_access_token', lambda: ''
        )
        stub_passback = stub_function(
            pylti1p3.assignments_grades.AssignmentsGradesService, 'put_grade',
            raise_pylti1p3_exc
        )

        course, course_conn = helpers.create_lti1p3_course(
            test_client, session, lti1p3_provider
        )
        assig = helpers.create_lti1p3_assignment(
            session, course, state='done', deadline=tomorrow
        )

        user = helpers.create_lti1p3_user(session, lti1p3_provider)
        course_conn.maybe_add_user_to_course(user, [])

        older_sub, newest_sub = [
            helpers.to_db_object(
                helpers.create_submission(test_client, assig, for_user=user),
                m.Work
            ) for _ in range(2)
        ]

    with describe('calling directly with non existing assignment is a noop'):
        m.LTI1p3Provider._passback_submission((newest_sub.id, 100000))
        assert not stub_passback.called

    with describe('changing grade of non newest sub does not passback'):
        older_sub.set_grade(5.0, admin_user)
        assert signal.was_send_once
        assert not stub_passback.called

    with describe('changing grade on newest sub passes the new grade back'):
        newest_sub.set_grade(9.5, m.User.resolve(admin_user))
        assert signal.was_send_once
        assert stub_passback.called_amount == 1
        assert stub_passback.args[0][0].get_score_given() == 9.5

    with describe('unsetting grade should passback something without grade'):
        newest_sub.set_grade(None, m.User.resolve(admin_user))

        assert signal.was_send_once
        assert stub_passback.called_amount == 1
        assert stub_passback.args[0][0].get_score_given() is None
        assert stub_passback.args[0][0].get_activity_progress() == 'Submitted'
Exemple #5
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
def test_passback_with_bonus_points(
    lti1p3_provider, describe, logged_in, admin_user, watch_signal,
    stub_function, test_client, session, tomorrow
):
    with describe('setup'), logged_in(admin_user):
        watch_signal(signals.WORK_CREATED, clear_all_but=[])
        watch_signal(signals.USER_ADDED_TO_COURSE, clear_all_but=[])
        signal = watch_signal(
            signals.GRADE_UPDATED,
            clear_all_but=[m.LTI1p3Provider._passback_submission]
        )

        stub_function(
            pylti1p3.service_connector.ServiceConnector,
            'get_access_token', lambda: ''
        )
        stub_passback = stub_function(
            pylti1p3.assignments_grades.AssignmentsGradesService, 'put_grade',
            raise_pylti1p3_exc
        )

        course, course_conn = helpers.create_lti1p3_course(
            test_client, session, lti1p3_provider
        )
        assig = helpers.create_lti1p3_assignment(
            session, course, state='done', deadline=tomorrow
        )
        test_client.req(
            'patch',
            f'/api/v1/assignments/{helpers.get_id(assig)}',
            200,
            data={'max_grade': 15}
        )

        user = helpers.create_lti1p3_user(session, lti1p3_provider)
        course_conn.maybe_add_user_to_course(user, [])

        sub = helpers.to_db_object(
            helpers.create_submission(test_client, assig, for_user=user),
            m.Work
        )

    with describe('can passback bonus points'), logged_in(admin_user):
        test_client.req(
            'patch',
            f'/api/v1/submissions/{helpers.get_id(sub)}',
            200,
            data={'grade': 14},
        )
        assert signal.was_send_once
        assert stub_passback.called_amount == 1

        assert stub_passback.args[0][0].get_score_given() == 14
        assert stub_passback.args[0][0].get_score_maximum() == 10
Exemple #7
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()]}
        )
def test_get_self_from_course(
    describe, admin_user, test_client, session, app, logged_in
):
    with describe('setup'), logged_in(admin_user):
        course = helpers.to_db_object(
            helpers.create_course(test_client), m.Course
        )
        lti_course = helpers.to_db_object(
            helpers.create_lti_course(session, app, admin_user), m.Course
        )

    with describe('Getting without course should return none'):
        assert m.LTI1p1Provider._get_self_from_course(None) is None

    with describe('Getting for non LTI course should return none'):
        assert m.LTI1p1Provider._get_self_from_course(course) is None

    with describe('Getting with LTI course should return instance'):
        prov = m.LTI1p1Provider._get_self_from_course(lti_course)
        assert prov is not None
        assert lti_course.lti_provider.id == prov.id

    with describe('Getting for for other LTI class should return none'):
        assert m.LTI1p3Provider._get_self_from_course(lti_course) is None
Exemple #9
0
    def _login(user, yield_token=False):
        setattr(ctx_stack.top, 'jwt_user', None)
        if isinstance(user, str) and user == 'NOT_LOGGED_IN':
            _TOKENS.append(None)
            res = None
        else:
            _TOKENS.append(
                helpers.to_db_object(user, m.User).make_access_token()
            )
            res = user

        yield _TOKENS[-1] if yield_token else res

        _TOKENS.pop(-1)
        setattr(ctx_stack.top, 'jwt_user', None)
def test_find_assignment_without_resource_id(
    lti1p3_provider, describe, make_function_spy, admin_user, logged_in,
    session, app
):
    with describe('setup'), logged_in(admin_user):
        lti_course = helpers.to_db_object(
            helpers.create_lti_course(session, app, admin_user), m.Course
        )
        spy = make_function_spy(
            flask_sqlalchemy.BaseQuery, 'filter', pass_self=True
        )

    with describe('find_assignment does not query without resource_id'):
        res = lti1p3_provider.find_assignment(lti_course, None, None)
        assert res is None
        assert not spy.called
Exemple #11
0
    def _login(user, yield_token=False):
        setattr(ctx_stack.top, 'jwt_user', None)
        if isinstance(user, str) and user == 'NOT_LOGGED_IN':
            _TOKENS.append(None)
            res = None
        else:
            _TOKENS.append(
                flask_jwt.create_access_token(
                    identity=helpers.to_db_object(user, m.User).id, fresh=True
                )
            )
            res = user

        yield _TOKENS[-1] if yield_token else res

        _TOKENS.pop(-1)
        setattr(ctx_stack.top, 'jwt_user', None)
Exemple #12
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,
            )
def test_failing_passback(
    lti1p3_provider, describe, logged_in, admin_user, watch_signal,
    stub_function, test_client, session, tomorrow
):
    with describe('setup'), logged_in(admin_user):
        watch_signal(signals.WORK_CREATED, clear_all_but=[])
        watch_signal(signals.GRADE_UPDATED, clear_all_but=[])
        watch_signal(signals.USER_ADDED_TO_COURSE, clear_all_but=[])
        stub_function(
            pylti1p3.service_connector.ServiceConnector,
            'get_access_token', lambda: ''
        )
        stub_passback = stub_function(
            pylti1p3.assignments_grades.AssignmentsGradesService, 'put_grade',
            raise_pylti1p3_exc
        )

        course, course_conn = helpers.create_lti1p3_course(
            test_client, session, lti1p3_provider
        )
        assig = helpers.create_lti1p3_assignment(
            session, course, state='done', deadline=tomorrow
        )

        user = helpers.create_lti1p3_user(session, lti1p3_provider)
        course_conn.maybe_add_user_to_course(user, [])

        sub = helpers.to_db_object(
            helpers.create_submission(test_client, assig, for_user=user),
            m.Work
        )
        sub.set_grade(10.0, m.User.resolve(admin_user))
        session.commit()
        hist = m.GradeHistory.query.filter_by(work=sub).one()
        assert hist
        assert not hist.passed_back

    with describe('failing passback should not update history'):

        m.LTI1p3Provider._passback_grades(assig.id)
        assert stub_passback.called
        hist = m.GradeHistory.query.filter_by(work=sub).one()
        assert hist
        assert not hist.passed_back
Exemple #14
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()}
        )
def test_has_permission_filter(describe, test_client, session, admin_user,
                               logged_in, perm):
    with describe('setup'), logged_in(admin_user):
        course = helpers.to_db_object(helpers.create_course(test_client),
                                      m.Course)

        rol1 = m.CourseRole('rol1', course, hidden=False)
        rol2 = m.CourseRole('rol2', course, hidden=False)
        session.add(rol1)
        session.add(rol2)
        session.commit()

        user1 = helpers.create_user_with_role(session, 'rol1', course)
        user2 = helpers.create_user_with_role(session, 'rol2', course)

        rol1.set_permission(perm, True)
        rol2.set_permission(perm, False)
        session.commit()

    with describe('Role is include if (and only if) the permission is true'):
        roles = m.CourseRole.get_roles_with_permission(perm).all()
        assert rol1 in roles
        assert rol2 not in roles

    with describe('Can be filtered further'):
        roles = m.CourseRole.get_roles_with_permission(perm).filter(
            m.CourseRole.course == course).all()
        assert all(r.course_id == course.id for r in roles)
        assert 'rol1' in [r.name for r in roles]
        assert 'Teacher' in [r.name for r in roles]

    with describe('User show up if permission is true not otherwise'):

        query = course.get_all_users_in_course(
            include_test_students=False,
            with_permission=perm,
        )
        assert sorted([user1.id,
                       admin_user.id]) == sorted(user.id for user, _ in query)
def test_retrieve_users_in_course(
    describe, lti1p3_provider, stub_function_class, monkeypatch, test_client,
    admin_user, logged_in, session, app, watch_signal
):
    with describe('setup'), logged_in(admin_user):
        course = helpers.to_db_object(
            helpers.create_course(test_client), m.Course
        )
        membership_url = f'http://{uuid.uuid4()}'
        lti_course, _ = helpers.create_lti1p3_course(
            test_client,
            session,
            lti1p3_provider,
            membership_url,
        )
        return_value = []

        stub_get = stub_function_class(lambda: copy.deepcopy(return_value))
        monkeypatch.setattr(
            pylti1p3.names_roles.NamesRolesProvisioningService, 'get_members',
            stub_get
        )

        assig_created_signal = watch_signal(
            # Make sure we flush the db as we expect that the created
            # assignment can be found by doing queries.
            signals.ASSIGNMENT_CREATED,
            flush_db=True
        )
        user_added_signal = watch_signal(
            signals.USER_ADDED_TO_COURSE,
            clear_all_but=[m.LTI1p3Provider._retrieve_users_in_course]
        )

        new_user_id1 = str(uuid.uuid4())
        new_user_id2 = str(uuid.uuid4())

        do_poll_again = True
        stub_poll_again = stub_function_class(lambda: do_poll_again)
        monkeypatch.setattr(
            m.CourseLTIProvider, 'can_poll_names_again', stub_poll_again
        )

    with describe('make sure it is connected to the necessary signals'):
        assert signals.USER_ADDED_TO_COURSE.is_connected(
            m.LTI1p3Provider._retrieve_users_in_course
        )
        assert signals.ASSIGNMENT_CREATED.is_connected(
            m.LTI1p3Provider._retrieve_users_in_course
        )

    with describe('non lti courses should be ignored'):
        m.LTI1p3Provider._retrieve_users_in_course(course.id)
        assert not stub_get.called

    with describe('should work when no members are returned'):
        assig = helpers.create_lti1p3_assignment(session, lti_course)
        assert assig_created_signal.was_send_once
        assert stub_get.called
        assert user_added_signal.was_not_send
        assert stub_poll_again.called

    with describe('Should be possible to add members'):
        return_value = [
            {
                'status': 'Active',
                # Not correct at all, but the function should still not crash.
                'message': object(),
                'user_id': new_user_id1,
                'email': '*****@*****.**',
                'name': 'USER1',
            },
            {
                'status': 'Active',
                'message': {
                    claims.CUSTOM: {'cg_username_0': 'username_user2'}
                },
                'user_id': new_user_id2,
                'email': '*****@*****.**',
                'name': 'USER2',
                'roles': ['Student'],
            },
        ]
        signals.ASSIGNMENT_CREATED.send(assig)
        assert stub_poll_again.called
        assert user_added_signal.was_send_once

        # USER1 should not be added and recursion should not happen
        assert user_added_signal.called_amount == 1
        assert m.User.query.filter_by(username='******'
                                      ).one().is_enrolled(lti_course)
        assert m.User.query.filter_by(email='*****@*****.**'
                                      ).one_or_none() is None

    with describe('Can add known users to new courses, even without username'):
        # Remove the message claim
        return_value = [{**r, 'message': {}} for r in return_value]

        with logged_in(admin_user):
            lti_course2, _ = helpers.create_lti1p3_course(
                test_client, session, lti1p3_provider
            )

        helpers.create_lti1p3_assignment(session, lti_course2)

        assert user_added_signal.was_send_once
        assert m.User.query.filter_by(username='******'
                                      ).one().is_enrolled(lti_course2)
        assert m.User.query.filter_by(email='*****@*****.**'
                                      ).one_or_none() is None

    with describe('if can poll return no poll should be done'):
        do_poll_again = False
        signals.ASSIGNMENT_CREATED.send(assig)
        assert not stub_get.called
Exemple #17
0
def test_valid_saml_flows(test_client, describe, logged_in, admin_user,
                          stub_function, app, session, monkeypatch):
    with describe('setup'), logged_in(admin_user):
        with open(helpers.test_data('test_saml_xml',
                                    'valid_with_logo.xml')) as f:
            xml = f.read()

        with open(helpers.test_data('test_saml_xml',
                                    'valid_response.json')) as f:
            valid_response = json.load(f)

        stub_function(m.saml_provider._MetadataParser, 'get_metadata',
                      lambda: xml)

        prov = helpers.to_db_object(
            helpers.create_sso_provider(test_client,
                                        stub_function,
                                        'Prov',
                                        no_stub=True), m.Saml2Provider)

        token = valid_response.pop('token')
        request_time = DatetimeWithTimezone.fromisoformat(
            valid_response.pop('request_time'))
        auth_n_request = valid_response.pop('AuthNRequest')
        prov.id = valid_response.pop('prov_id')
        prov._key_data = valid_response.pop('key').encode('utf8')
        prov._cert_data = valid_response.pop('cert').encode('utf8')

        host = valid_response['http_host']
        http = 'https' if valid_response['https'] == 'on' else 'http'
        external_url = furl.furl()
        external_url.scheme = http
        external_url.host = host

        session.commit()
        stub_function(psef.saml2, '_prepare_flask_request',
                      lambda: valid_response)

    with test_client as client, freezegun.freeze_time(request_time):
        with describe('can do login when all is good'):
            client.get(f'/api/sso/saml2/login/{helpers.get_id(prov)}')
            with client.session_transaction() as sess:
                sess['[SAML]_AuthNRequestID'] = auth_n_request
                sess['[SAML]_TOKEN'] = token

            with monkeypatch.context() as ctx:
                ctx.setitem(app.config, 'EXTERNAL_URL', external_url.tostr())

                res = client.post(f'/api/sso/saml2/acs/{helpers.get_id(prov)}',
                                  data='Not used')

            assert 300 <= res.status_code < 400
            loc = res.headers['Location']

            base = furl.furl(external_url.tostr()).add(path='/sso_login/')

            assert loc.startswith(base.tostr())
            assert token == loc.split('/')[-1]
            db_base_id = flask.session['[SAML]_DB_BLOB_ID']

            assert m.BlobStorage.query.get(db_base_id) is not None

        with describe('cannot use jwt route without token in session'):
            with client.session_transaction() as sess:
                assert sess.pop('[SAML]_TOKEN') == token

            err = client.req('post', f'/api/sso/saml2/jwts/{token}', 409)
            assert 'Could not find all required data' in err['message']

        with describe('cannot use jwt route without correct token in session'):
            with client.session_transaction() as sess:
                sess['[SAML]_TOKEN'] = 'incorrect_token'
                sess['[SAML]_DB_BLOB_ID'] = db_base_id

            assert m.BlobStorage.query.get(db_base_id) is not None
            err = client.req('post', f'/api/sso/saml2/jwts/{token}', 400)
            assert 'Invalid token provided' in err['message']

        with describe(
                'cannot use jwt route without correct token in url and session'
        ):
            wrong_token = str(uuid.uuid4())
            with client.session_transaction() as sess:
                sess['[SAML]_TOKEN'] = wrong_token
                sess['[SAML]_DB_BLOB_ID'] = db_base_id

            err = client.req('post', f'/api/sso/saml2/jwts/{wrong_token}', 400)
            assert 'Invalid token provided' in err['message']

        with describe('cannot use jwt route without correct token in url'):
            with client.session_transaction() as sess:
                sess['[SAML]_TOKEN'] = token
                sess['[SAML]_DB_BLOB_ID'] = db_base_id

            err = client.req('post', f'/api/sso/saml2/jwts/{wrong_token}', 400)
            assert 'Invalid token provided' in err['message']

        with describe('cannot use blob after some time'
                      ), freezegun.freeze_time(request_time +
                                               timedelta(minutes=15)):
            with client.session_transaction() as sess:
                sess['[SAML]_TOKEN'] = token
                sess['[SAML]_DB_BLOB_ID'] = db_base_id

            client.req('post', f'/api/sso/saml2/jwts/{token}', 404)

        with describe('can use one time if all is right'):
            with client.session_transaction() as sess:
                sess['[SAML]_TOKEN'] = token
                sess['[SAML]_DB_BLOB_ID'] = db_base_id

            res = client.req('post', f'/api/sso/saml2/jwts/{token}', 200)
            assert isinstance(res.get('access_token'), str)

            # Cannot use again
            with client.session_transaction() as sess:
                sess['[SAML]_TOKEN'] = token
                sess['[SAML]_DB_BLOB_ID'] = db_base_id
            client.req('post', f'/api/sso/saml2/jwts/{token}', 404)
Exemple #18
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
Exemple #19
0
def test_giving_peer_feedback_comments(
    test_client, admin_user, session, describe, logged_in, yesterday, tomorrow,
    auto_approved, make_add_reply
):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assignment = helpers.create_assignment(
            test_client, course, deadline=tomorrow, state='open'
        )
        teacher = helpers.create_user_with_role(session, 'Teacher', course)
        users = [
            helpers.get_id(
                helpers.create_user_with_role(session, 'Student', course)
            ) for _ in range(5)
        ]
        user1, *other_users = users
        subs_by_user = {}
        for user in users:
            helpers.create_submission(test_client, assignment, for_user=user)
            # Every user has two submissions
            subs_by_user[user] = helpers.create_submission(
                test_client, assignment, for_user=user
            )

        helpers.enable_peer_feedback(
            test_client, assignment, amount=1, auto_approved=auto_approved
        )
        conns = get_all_connections(assignment, 1)[user1]
        other_user = next(u for u in other_users if u not in conns)
        base_url = f'/api/v1/assignments/{helpers.get_id(assignment)}'
        pf_sub = subs_by_user[conns[0]]
        own_sub = subs_by_user[user1]
        add_reply = make_add_reply(pf_sub)

        with logged_in(teacher):
            teacher_rep = add_reply('Hello')
            base_comment_id = teacher_rep['comment_base_id']
            teacher_rep.delete()

    with describe(
        'Before deadline we have no connections (and cannot place feedback)'
    ), logged_in(user1):
        test_client.req(
            'get',
            f'{base_url}/submissions/?latest_only',
            200,
            result=[{
                'id': int,
                'user': {
                    'id': user1,
                    '__allow_extra__': True,
                },
                '__allow_extra__': True,
            }]
        )
        add_reply(
            'Too early for a reply', expect_error=403, base_id=base_comment_id
        )

    with logged_in(teacher):
        test_client.req(
            'patch', base_url, 200, data={'deadline': yesterday.isoformat()}
        )

    with describe(
        'Will receive peer feedback submissions when getting all submissions'
    ), logged_in(user1):
        test_client.req(
            'get',
            f'{base_url}/submissions/?latest_only&extended',
            200,
            result=[pf_sub, own_sub],
        )
        # Can also get older subs of a assigned user
        test_client.req(
            'get',
            f'{base_url}/users/{conns[0]}/submissions/?extended',
            200,
            result=[
                pf_sub,
                {
                    '__allow_extra__': True,
                    'user': {'id': conns[0], '__allow_extra__': True},
                }
            ],
        )

    with describe('Can comment on other submission'), logged_in(user1):
        reply = add_reply('A peer feedback comment', expect_peer_feedback=True)
        assert reply['approved'] == auto_approved

    with describe('Cannot change approval status yourself'), logged_in(user1):
        reply.set_approval(not auto_approved, err=403)

    with describe('Teacher can change approval status'), logged_in(teacher):
        reply = reply.set_approval(not auto_approved)

    with describe('Editing feedback resets approval status'):
        with logged_in(user1):
            # We don't reset it back to ``True`` if ``auto_approved`` is
            # ``True`` but we do set it back to ``False``.
            reply = reply.update('New content', approved=False)

        if auto_approved:
            # If reply is approved an ``auto_approved`` is ``True`` the
            # approval state should not change.
            with logged_in(teacher):
                reply.set_approval(True)
            with logged_in(user1):
                reply = reply.update('New content2', approved=True)

    with describe(
        'Cannot place or edit peer feedback after pf deadline has expired'
    ):
        with logged_in(teacher):
            helpers.enable_peer_feedback(
                test_client,
                assignment,
                amount=1,
                auto_approved=auto_approved,
                days=0.5
            )
            assert get_all_connections(assignment, 1)[user1] == conns
        with logged_in(user1):
            add_reply('Another peer feedback comment', expect_error=403)

            reply.update('Cannot update!', err=403)

    with describe('Can always place peer feedback if pf time is None'):
        with logged_in(teacher):
            helpers.enable_peer_feedback(
                test_client,
                assignment,
                amount=1,
                auto_approved=auto_approved,
                days=None,
            )

        with freezegun.freeze_time(
            DatetimeWithTimezone.utcnow() + timedelta(days=365 * 20)
        ), logged_in(user1):
            reply = reply.update('Can update way after the deadline!')

    with describe('Cannot add peer feedback to a non assigned sub'
                  ), logged_in(other_user):
        add_reply(
            'Not possible to add this',
            expect_error=403,
            base_id=reply['comment_base_id']
        )

    with describe('Student can see approved peer feedback after done'):
        with logged_in(teacher):
            reply = reply.set_approval(not auto_approved)
            test_client.req(
                'get',
                f'/api/v1/submissions/{helpers.get_id(pf_sub)}/feedbacks/',
                200,
                query={'with_replies': '1'},
                result={
                    'general': '',
                    'linter': {},
                    'user': [{
                        'id': int,
                        '__allow_extra__': True,
                        'replies': [helpers.dict_without(reply, 'author'), ],
                    }],
                    'authors': [helpers.to_db_object(user1, m.User)],
                },
            )
            test_client.req('patch', base_url, 200, data={'state': 'done'})

        with logged_in(conns[0]):
            test_client.req(
                'get',
                f'/api/v1/submissions/{helpers.get_id(pf_sub)}/feedbacks/',
                200,
                query={'with_replies': '1'},
                result={
                    'general': '',
                    'linter': {},
                    'authors': ([helpers.to_db_object(user1, m.User)]
                                if reply['approved'] else []),
                    'user': ([{
                        'id': int,
                        '__allow_extra__': True,
                        'replies': [helpers.dict_without(reply, 'author')],
                    }] if reply['approved'] else []),
                },
            )
Exemple #20
0
def test_disabling_peer_feedback(
    test_client, admin_user, session, describe, logged_in, yesterday,
    make_add_reply
):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assignment = helpers.create_assignment(
            test_client, course, deadline=yesterday, state='open'
        )
        teacher = helpers.create_user_with_role(session, 'Teacher', course)
        users = [
            helpers.get_id(
                helpers.create_user_with_role(session, 'Student', course)
            ) for _ in range(5)
        ]
        user1, *other_users = users
        for user in users:
            helpers.create_submission(test_client, assignment, for_user=user)

        helpers.enable_peer_feedback(test_client, assignment)
        conns = get_all_connections(assignment, 1)[user1]
        base_url = f'/api/v1/assignments/{helpers.get_id(assignment)}'

        pf_sub = m.Work.query.filter_by(
            assignment_id=helpers.get_id(assignment),
            user_id=helpers.get_id(conns[0]),
        ).one()
        pf_url = f'{base_url}/peer_feedback_settings'

        add_reply = make_add_reply(pf_sub)
        with logged_in(user1):
            reply = add_reply(
                'A peer feedback comment', expect_peer_feedback=True
            )

    with describe('Students cannot disable peer feedback'), logged_in(user1):
        test_client.req('delete', pf_url, 403)

    with describe('Teachers can disable peer feedback'), logged_in(teacher):
        test_client.req('delete', pf_url, 204)
        get_all_connections(assignment, 0)

    with describe('Old feedback still exists after disabling'
                  ), logged_in(teacher):
        test_client.req(
            'get',
            f'/api/v1/submissions/{helpers.get_id(pf_sub)}/feedbacks/',
            200,
            query={'with_replies': '1'},
            result={
                'general': '',
                'linter': {},
                'authors': [helpers.to_db_object(user1, m.User)],
                'user': [{
                    'id': int,
                    '__allow_extra__': True,
                    'replies': [helpers.dict_without(reply, 'author')],
                }],
            }
        )

        # Students can no longer see their peer feedback subs
    with describe('Student can no longer place new comments'
                  ), logged_in(user1):
        add_reply(
            'No peer feedback',
            expect_error=403,
            base_id=reply['comment_base_id'],
        )

    with describe('deleting again does nothing'), logged_in(teacher):
        test_client.req('delete', pf_url, 204)
        get_all_connections(assignment, 0)
def test_passing_back_all_grades(
    lti1p3_provider, describe, logged_in, admin_user, watch_signal,
    stub_function, test_client, session, tomorrow, make_function_spy
):
    with describe('setup'), logged_in(admin_user):
        watch_signal(signals.WORK_CREATED, clear_all_but=[])
        watch_signal(signals.GRADE_UPDATED, clear_all_but=[])
        watch_signal(signals.USER_ADDED_TO_COURSE, clear_all_but=[])
        watch_signal(signals.WORK_DELETED, clear_all_but=[])
        signal = watch_signal(
            signals.ASSIGNMENT_STATE_CHANGED,
            clear_all_but=[m.LTI1p3Provider._passback_grades]
        )

        stub_get_acccess_token = stub_function(
            pylti1p3.service_connector.ServiceConnector,
            'get_access_token', lambda: ''
        )
        stub_passback = make_function_spy(
            pylti1p3.assignments_grades.AssignmentsGradesService,
            'put_grade',
            pass_self=True
        )

        # Make a session with a response that returns json when the `json`
        # method is called
        req_session = requests_stubs.session_maker()()
        req_session.Response.json = lambda _=None: {}
        stub_requests_post = stub_function(requests, 'post', req_session.post)

        course, course_conn = helpers.create_lti1p3_course(
            test_client, session, lti1p3_provider
        )
        assig = helpers.create_lti1p3_assignment(
            session, course, state='hidden', deadline=tomorrow
        )

        user1 = helpers.create_lti1p3_user(session, lti1p3_provider)
        user2 = helpers.create_lti1p3_user(session, lti1p3_provider)
        user3 = helpers.create_lti1p3_user(session, lti1p3_provider)
        all_students = [user1, user2, user3]

        for u in all_students:
            course_conn.maybe_add_user_to_course(u, ['Learner'])

        lti_user_ids = [
            m.UserLTIProvider.query.filter_by(user=u).one().lti_user_id
            for u in all_students
        ]

        gset = helpers.create_group_set(test_client, course, 1, 2, [assig])
        helpers.create_group(test_client, gset, [user1, user3])

        sub = helpers.to_db_object(
            helpers.create_submission(test_client, assig, for_user=user1),
            m.Work
        )
        sub.set_grade(2.5, admin_user)

        # Make sure user2 does not have a non deleted submission
        user2_sub = helpers.create_submission(
            test_client, assig, for_user=user2
        )
        test_client.req(
            'delete', f'/api/v1/submissions/{helpers.get_id(user2_sub)}', 204
        )

    with describe('changing assignment state to "open" does not passback'):
        assig.set_state_with_string('open')
        assert signal.was_send_once
        assert not stub_passback.called
        assert not stub_get_acccess_token.called

    with describe('changing to done does passback'):
        assig.set_state_with_string('done')
        assert signal.was_send_once
        assert stub_passback.called_amount == len(all_students)
        # Calls should be cached
        assert stub_get_acccess_token.called_amount == 1

        p1, p2, p3 = stub_passback.args

        assert p1[0] != p2[0] != p3[0]

        assert p1[0].get_score_given() == 2.5
        assert p2[0].get_score_given() == 2.5
        assert {p1[0].get_user_id(),
                p2[0].get_user_id()} == {lti_user_ids[0], lti_user_ids[2]}

        # Does not have a submission
        assert p3[0].get_score_given() is None
        assert p3[0].get_user_id() == lti_user_ids[1]

    with describe('toggling to open and done should do a new passback'):
        assig.set_state_with_string('open')
        assert signal.was_send_once
        assert not stub_passback.called
        assert not stub_get_acccess_token.called

        assig.set_state_with_string('done')
        assert signal.was_send_n_times(2)
        assert stub_passback.called
        # access token should still be cached
        assert not stub_get_acccess_token.called
def test_delete_submission_passback(
    lti1p3_provider, describe, logged_in, admin_user, watch_signal,
    stub_function, test_client, session, tomorrow
):
    with describe('setup'), logged_in(admin_user):
        watch_signal(signals.WORK_CREATED, clear_all_but=[])
        watch_signal(signals.GRADE_UPDATED, clear_all_but=[])
        watch_signal(signals.USER_ADDED_TO_COURSE, clear_all_but=[])
        stub_function(
            pylti1p3.service_connector.ServiceConnector,
            'get_access_token', lambda: ''
        )
        stub_passback = stub_function(
            pylti1p3.assignments_grades.AssignmentsGradesService, 'put_grade'
        )

        course, course_conn = helpers.create_lti1p3_course(
            test_client, session, lti1p3_provider
        )
        assig = helpers.create_lti1p3_assignment(
            session, course, state='done', deadline=tomorrow
        )
        user = helpers.create_lti1p3_user(session, lti1p3_provider)
        lti_user_id = m.UserLTIProvider.query.filter_by(
            user=m.User.resolve(user)
        ).one().lti_user_id
        course_conn.maybe_add_user_to_course(user, ['Learner'])

        sub_oldest, sub_older, sub_middle, sub_newest = [
            helpers.to_db_object(
                helpers.create_submission(test_client, assig, for_user=user),
                m.Work
            ) for _ in range(4)
        ]
        signal = watch_signal(
            signals.WORK_DELETED,
            clear_all_but=[m.LTI1p3Provider._delete_submission]
        )

        def do_delete(sub):
            with logged_in(admin_user):
                test_client.req(
                    'delete', f'/api/v1/submissions/{helpers.get_id(sub)}', 204
                )

    with describe('Delete non newest'):
        do_delete(sub_older)
        assert signal.was_send_once
        assert not stub_passback.called

    with describe('Calling method for non existing work simply does nothing'):
        m.LTI1p3Provider._delete_submission(1000000)
        assert not stub_passback.called

    with describe('Calling method for non deleted work does nothing'):
        m.LTI1p3Provider._delete_submission(helpers.get_id(sub_newest))
        assert not stub_passback.called

    with describe('Delete newest should passback grade of new newest'):
        sub_middle.set_grade(5.0, m.User.resolve(admin_user))
        session.commit()
        # We should have removed the grade_updated signal
        assert not stub_passback.called

        do_delete(sub_newest)
        assert signal.was_send_once
        assert stub_passback.called_amount == 1
        grade, = stub_passback.all_args[0].values()
        assert grade.get_score_given() == 5.0
        assert grade.get_user_id() == lti_user_id

    with describe('Deleting new newest should passback next non deleted'):
        sub_older.set_grade(6.0, m.User.resolve(admin_user))
        sub_oldest.set_grade(8.0, m.User.resolve(admin_user))
        session.commit()
        assert not m.GradeHistory.query.filter_by(work=sub_oldest
                                                  ).one().passed_back

        do_delete(sub_middle)
        assert signal.was_send_once
        assert stub_passback.called_amount == 1
        grade, = stub_passback.all_args[0].values()
        # Should passback oldest as we deleted older in an earlier block
        assert grade.get_score_given() == 8.0
        assert grade.get_user_id() == lti_user_id

        # Should update history
        assert m.GradeHistory.query.filter_by(work=sub_oldest
                                              ).one().passed_back

    with describe('Deleting without any existing submission should passback'):
        do_delete(sub_oldest)
        assert signal.was_send_once
        assert stub_passback.called_amount == 1
        grade, = stub_passback.all_args[0].values()
        assert grade.get_score_given() is None
        assert grade.get_grading_progress() == 'NotReady'
        assert grade.get_user_id() == lti_user_id

    with describe('Passing back deleted sub should do nothing'):
        lti1p3_provider._passback_grade(
            assignment=assig,
            sub=sub_newest,
            timestamp=DatetimeWithTimezone.utcnow()
        )
        assert not stub_passback.called