Example #1
0
def test_reply_to_a_comment(logged_in, test_client, session, admin_user,
                            mail_functions, describe, tomorrow,
                            make_add_reply):
    with describe('setup'), logged_in(admin_user):
        assignment = helpers.create_assignment(test_client,
                                               state='open',
                                               deadline=tomorrow)
        course = assignment['course']
        teacher = admin_user
        student = helpers.create_user_with_role(session, 'Student', course)
        ta = helpers.create_user_with_role(session, 'TA', course)

        work_id = helpers.get_id(
            helpers.create_submission(test_client,
                                      assignment,
                                      for_user=student))

        add_reply = make_add_reply(work_id)
        feedback_url = f'/api/v1/submissions/{work_id}/feedbacks/?with_replies'
        with logged_in(student):
            reply = add_reply('a reply')

    with describe('You set the in_reply_to of a comment'), logged_in(teacher):
        add_reply('a reply to', in_reply_to=reply)

    with describe('You cannot reply on a comment from a different line'
                  ), logged_in(teacher):
        add_reply('a reply to', line=10, in_reply_to=reply, expect_error=404)
Example #2
0
def test_searching_test_student(
    logged_in, session, error_template, test_client, request, admin_user,
    ta_user, tomorrow, assignment
):
    with logged_in(admin_user):
        course = helpers.create_course(test_client)
        teacher = helpers.create_user_with_role(session, 'Teacher', course)

        actual_student_name = f'TEST_STUDENT_{str(uuid.uuid4())}'
        student = helpers.create_user_with_role(
            session, 'Student', course, name=actual_student_name
        )

    with logged_in(teacher):
        assig_id = helpers.create_assignment(
            test_client, course, 'open', deadline=tomorrow
        )['id']

        res = helpers.create_submission(
            test_client,
            assignment_id=assig_id,
            is_test_submission=True,
        )
        test_user_id = res['user']['id']

        res = test_client.req(
            'get',
            '/api/v1/users/?q=TEST_STUDENT',
            200,
        )

        assert res
        for user in res:
            assert user['id'] != test_user_id
        assert student.id in set(user['id'] for user in res)
Example #3
0
def test_proxy_with_submission(test_client, logged_in, describe, session,
                               admin_user):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assig = helpers.create_assignment(test_client,
                                          course,
                                          state='open',
                                          deadline='tomorrow')
        stud = helpers.create_user_with_role(session, 'Student', [course])
        stud2 = helpers.create_user_with_role(session, 'Student', [course])

        sub_data = (
            (f'{os.path.dirname(__file__)}/../test_data/test_submissions/'
             'html.tar.gz'),
            'f.tar.gz',
        )
        s1 = helpers.create_submission(test_client,
                                       assig,
                                       submission_data=sub_data,
                                       for_user=stud)['id']
        s2 = helpers.create_submission(test_client,
                                       assig,
                                       submission_data=sub_data,
                                       for_user=stud2)['id']

        data = {
            'allow_remote_resources': True,
            'allow_remote_scripts': True,
            'teacher_revision': False,
        }

    with describe('Students can only create proxy when they may see files'
                  ), logged_in(stud):
        test_client.req('post',
                        f'/api/v1/submissions/{s1}/proxy',
                        200,
                        data=data)
        test_client.req('post',
                        f'/api/v1/submissions/{s2}/proxy',
                        403,
                        data=data)

        # Student has no permission for teacher files
        data['teacher_revision'] = True
        test_client.req('post',
                        f'/api/v1/submissions/{s1}/proxy',
                        403,
                        data=data)
        data['teacher_revision'] = False

    with describe('remote_scripts and remote_resources connection'), logged_in(
            stud):
        data['allow_remote_resources'] = False
        err = test_client.req('post',
                              f'/api/v1/submissions/{s1}/proxy',
                              400,
                              data=data)
        assert 'The value "allow_remote_scripts" can only be true' in err[
            'message']
Example #4
0
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=[])
Example #5
0
def basic(logged_in, admin_user, test_client, session):
    print(m.User.query.all())
    with logged_in(admin_user):
        course = helpers.create_course(test_client)
        assig_id = helpers.create_assignment(test_client,
                                             course,
                                             deadline=datetime.utcnow() +
                                             timedelta(days=30))['id']
        teacher = helpers.create_user_with_role(session, 'Teacher', [course])
        student = helpers.create_user_with_role(session, 'Student', [course])
    yield course, assig_id, teacher, student
def test_maybe_add_user_to_course(describe, lti1p3_provider, test_client,
                                  admin_user, logged_in, session,
                                  watch_signal):
    with describe('setup'), logged_in(admin_user):
        course, conn = helpers.create_lti1p3_course(test_client, session,
                                                    lti1p3_provider)
        signal = watch_signal(signals.USER_ADDED_TO_COURSE, clear_all_but=[])
        user = helpers.create_user_with_role(session, 'Teacher', [])
        user2 = helpers.create_user_with_role(session, 'Teacher', [])
        user3 = helpers.create_user_with_role(session, 'Teacher', [])
        user4 = helpers.create_user_with_role(session, 'Teacher', [])
        user5 = helpers.create_user_with_role(session, 'Teacher', [])
        student_role = m.CourseRole.query.filter_by(name='Student',
                                                    course=course).one()
        assert student_role is not None

    with describe('adding user without roles claim always creates a new role'):
        conn.maybe_add_user_to_course(user, [])
        assert signal.was_send_once
        assert user.is_enrolled(course)
        assert user.courses[course.id].name == 'New LTI Role'

        signal.reset()
        conn.maybe_add_user_to_course(user2, [])
        assert signal.was_send_once
        assert user2.is_enrolled(course)
        assert user2.courses[course.id].name == 'New LTI Role (1)'
        assert user2.courses[course.id].id != user.courses[course.id].id

    with describe('user already in course does nothing'):
        conn.maybe_add_user_to_course(user, ['Learner'])
        assert signal.was_not_send
        assert user.courses[course.id].name == 'New LTI Role'

    with describe('adding user with known role uses that role'):
        conn.maybe_add_user_to_course(user3, ['Learner'])
        assert signal.was_send_once
        assert user3.is_enrolled(course)
        assert user3.courses[course.id].id == student_role.id

    with describe(
            'Using unmapped role creates a new role if it does not exist'):
        conn.maybe_add_user_to_course(user4, ['Student', 'Other role'])
        assert signal.was_send_once
        assert user4.is_enrolled(course)
        assert user4.courses[course.id].name == 'Unmapped LTI Role (Student)'

        signal.reset()
        conn.maybe_add_user_to_course(user5, ['Other role', 'Student'])
        assert signal.was_send_once
        assert user5.is_enrolled(course)
        assert user5.courses[course.id].name == 'Unmapped LTI Role (Student)'
Example #7
0
def test_searching_test_student_in_course(logged_in, test_client, assignment,
                                          session, teacher_user, tomorrow):
    c_id = assignment.course_id

    with logged_in(teacher_user):
        actual_student_name = f'TEST_STUDENT_{str(uuid.uuid4())}'
        student = create_user_with_role(
            session,
            'Student',
            c_id,
            name=actual_student_name,
        )

        assig_id = create_assignment(test_client,
                                     c_id,
                                     'open',
                                     deadline=tomorrow)['id']

        res = create_submission(
            test_client,
            assig_id,
            is_test_submission=True,
        )
        test_user_id = res['user']['id']

        res = test_client.req(
            'get',
            f'/api/v1/courses/{c_id}/users/?q=TEST_STUDENT',
            200,
        )

        assert res
        for user in res:
            assert user['id'] != test_user_id
        assert student.id in set(user['id'] for user in res)
Example #8
0
def test_peer_feedback_and_test_student(
    test_client, logged_in, describe, admin_user, session, tomorrow,
    enable_after, student_amount
):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assignment = helpers.create_assignment(
            test_client, course, deadline=tomorrow
        )
        if not enable_after:
            helpers.enable_peer_feedback(test_client, assignment)

        users = [
            helpers.get_id(
                helpers.create_user_with_role(session, 'Student', course)
            ) for _ in range(student_amount)
        ]
        helpers.create_submission(
            test_client, assignment, is_test_submission=True
        )
        for user in users:
            helpers.create_submission(test_client, assignment, for_user=user)

        if enable_after:
            helpers.enable_peer_feedback(test_client, assignment)

    with describe('nobody should be connected to the test student'
                  ), logged_in(admin_user):
        conns = get_all_connections(assignment, 1)
        assert len(conns) == student_amount
        for user in users:
            assert user in conns
            assert all(conn in users for conn in users)
Example #9
0
def test_set_setting(test_client, describe, logged_in, admin_user, session):
    with describe('setup'):
        user = helpers.create_user_with_role(session, 'Teacher', [])
        orig = site_settings.Opt.MIN_PASSWORD_SCORE.value
        assert orig not in (1, 2)

        def make_data(name, value):
            return {'updates': [{'name': name, 'value': value}]}

    with describe('cannot set settings as normal user'):
        test_client.req('patch', '/api/v1/site_settings/', 401)
        with logged_in(user):
            test_client.req('patch',
                            '/api/v1/site_settings/',
                            403,
                            data={'updates': []})

    with describe('can set settings as admin user'), logged_in(admin_user):
        test_client.req(
            'patch',
            '/api/v1/site_settings/',
            200,
            data=make_data('MIN_PASSWORD_SCORE', 2),
            result={
                '__allow_extra__': True,
                'MIN_PASSWORD_SCORE': 2
            },
        )
        assert site_settings.Opt.MIN_PASSWORD_SCORE.value == 2

        # Can mutate it
        test_client.req(
            'patch',
            '/api/v1/site_settings/',
            200,
            data=make_data('MIN_PASSWORD_SCORE', 1),
            result={
                '__allow_extra__': True,
                'MIN_PASSWORD_SCORE': 1
            },
        )
        assert site_settings.Opt.MIN_PASSWORD_SCORE.value == 1

    with describe('Can reset back to original value'), logged_in(admin_user):
        test_client.req(
            'patch',
            '/api/v1/site_settings/',
            200,
            data=make_data('MIN_PASSWORD_SCORE', None),
            result={
                '__allow_extra__': True,
                'MIN_PASSWORD_SCORE': orig
            },
        )
        assert site_settings.Opt.MIN_PASSWORD_SCORE.value == orig
Example #10
0
def test_division_larger_assignment(
    test_client, admin_user, session, describe, logged_in, yesterday, students
):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assignment = helpers.create_assignment(
            test_client, course, deadline=yesterday
        )
        all_users = [
            helpers.get_id(
                helpers.create_user_with_role(session, 'Student', course)
            ) for _ in range(students)
        ]
        user1, *rest_users = all_users
        rest_subs = [(
            user,
            helpers.create_submission(test_client, assignment, for_user=user)
        ) for user in rest_users]

    with describe('Enabling peer feedback should do divide'
                  ), logged_in(admin_user):
        helpers.enable_peer_feedback(test_client, assignment, amount=3)
        conns = get_all_connections(assignment, 3)
        assert len(conns) == len(rest_users)

    with describe('Submitting should still be possible'
                  ), logged_in(admin_user):
        helpers.create_submission(test_client, assignment, for_user=user1)
        conns = get_all_connections(assignment, 3)
        assert len(conns) == len(all_users)
        assert len(conns[user1]) == 3

    with describe('Can delete all rest users'), logged_in(admin_user):
        warning_amount = 0
        for idx, (user_id, sub) in enumerate(rest_subs):
            _, rv = test_client.req(
                'delete',
                f'/api/v1/submissions/{helpers.get_id(sub)}',
                204,
                include_response=True,
            )
            had_warning = 'warning' in rv.headers
            if had_warning:
                warning_amount += 1
            print('Deleting submission of', user_id, 'warning:', had_warning)
            conns = get_all_connections(assignment, 3)
            left = len(all_users) - (idx + 1)
            if left > 3:
                assert len(conns) == left, f'Got wrong amount of conns {conns}'
            else:
                assert len(conns) == 0, f'Got wrong amount of conns {conns}'

        assert warning_amount < len(all_users)
Example #11
0
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)
Example #12
0
def basic(logged_in, admin_user, test_client, session):
    with logged_in(admin_user):
        course = helpers.create_course(test_client)
        assig_id = helpers.create_assignment(
            test_client,
            course,
            deadline=DatetimeWithTimezone.utcnow() + timedelta(days=30),
            state='open',
        )['id']
        assig = m.Assignment.query.get(assig_id)
        assert assig is not None
        teacher = helpers.create_user_with_role(session, 'Teacher', [course])
        student = helpers.create_user_with_role(session, 'Student', [course])
        with logged_in(teacher):
            test_client.req(
                'patch',
                f'/api/v1/assignments/{assig_id}',
                200,
                data={
                    'files_upload_enabled': True,
                    'webhook_upload_enabled': True,
                },
            )
    yield course, assig, teacher, student
Example #13
0
def test_direct_emails_in_thread(logged_in, test_client, session, admin_user,
                                 mail_functions, describe, tomorrow,
                                 make_add_reply):
    with describe('setup'), logged_in(admin_user):
        assignment = helpers.create_assignment(test_client,
                                               state='open',
                                               deadline=tomorrow)
        course = assignment['course']
        teacher = admin_user
        student = helpers.create_user_with_role(session, 'Student', course)

        work_id = helpers.get_id(
            helpers.create_submission(test_client,
                                      assignment,
                                      for_user=student))

        add_reply = make_add_reply(work_id)
        add_reply('base comment', include_response=True)

    with describe('Initial reply should not be send in a thread'), logged_in(
            student):
        add_reply('first reply')
        first_msg, = mail_functions.assert_mailed(teacher)
        assert 'first reply' in first_msg.msg
        assert student.name in first_msg.msg
        assert first_msg.in_reply_to is None

    with describe('Own replies should not be mailed'), logged_in(teacher):
        add_reply('own reply')
        mail_functions.assert_mailed(teacher, amount=0)

    with describe('Second reply should be send as reply to first message'
                  ), logged_in(student):
        add_reply('second reply')
        second_msg, = mail_functions.assert_mailed(teacher)
        assert 'first reply' not in second_msg.msg
        assert 'second reply' in second_msg.msg
        assert student.name in second_msg.msg
        assert second_msg.in_reply_to == first_msg.message_id
        assert second_msg.references == [first_msg.message_id]

    with describe('Third reply should be send as reply to first message'
                  ), logged_in(student):
        add_reply('third reply')
        third_msg, = mail_functions.assert_mailed(teacher)
        assert third_msg.in_reply_to == first_msg.message_id
        assert third_msg.references == [first_msg.message_id]
Example #14
0
def test_get_all_settings(test_client, describe, logged_in, admin_user,
                          session):
    with describe('setup'):
        user = helpers.create_user_with_role(session, 'Teacher', [])

    with describe('cannot get settings as normal user'):
        test_client.req('get', '/api/v1/site_settings/', 401)
        with logged_in(user):
            test_client.req('get', '/api/v1/site_settings/', 403)

    with describe('can get settings as admin user'), logged_in(admin_user):
        setting = test_client.req(
            'get',
            '/api/v1/site_settings/',
            200,
            result={
                setting.name: object
                for setting in site_settings.Opt._ALL_OPTS
            })

        for name, value in setting.items():
            print(name)
            getattr(site_settings.Opt, name).parser.try_parse(value)
Example #15
0
def test_send_login_links(describe, session, test_client, logged_in,
                          admin_user, tomorrow, stub_function, yesterday):
    with describe('setup'), logged_in(admin_user):
        stub_apply = stub_function(psef.tasks._send_login_links_to_users_1,
                                   'apply_async')
        stub_mail = stub_function(psef.mail, 'send_login_link_mail')

        assig = helpers.create_assignment(test_client)
        assig_id = helpers.get_id(assig)
        login_links_token = uuid.uuid4()
        m.Assignment.query.filter_by(id=assig_id).update({
            '_available_at':
            tomorrow.isoformat(),
            '_deadline': (tomorrow + timedelta(minutes=1)).isoformat(),
            '_send_login_links_token':
            login_links_token,
            'kind':
            'exam',
        })

        task_result = m.TaskResult(user=None)
        session.add(task_result)
        session.commit()

        student = helpers.create_user_with_role(session, 'Student',
                                                assig['course'])

    with describe('does not crash if task does not exist'):
        psef.tasks._send_login_links_to_users_1(assig_id,
                                                uuid.uuid4().hex,
                                                tomorrow.isoformat(),
                                                login_links_token, 1)
        assert task_result.state.is_not_started

    with describe('reschedules when called too early'):
        psef.tasks._send_login_links_to_users_1(assig_id, task_result.id.hex,
                                                tomorrow.isoformat(),
                                                login_links_token.hex, 1)

        assert stub_apply.called_amount == 1
        assert task_result.state.is_not_started

    with describe('Does nothing when token is different'):
        psef.tasks._send_login_links_to_users_1(assig_id, task_result.id.hex,
                                                yesterday.isoformat(),
                                                uuid.uuid4().hex, 1)
        assert not stub_mail.called
        assert task_result.state.is_skipped
        task_result.state = m.TaskResultState.not_started
        session.commit()

    with describe('Does nothing when deadline expired'):
        del flask.g.request_start_time
        with freeze_time(tomorrow + timedelta(days=1)):
            psef.tasks._send_login_links_to_users_1(assig_id,
                                                    task_result.id.hex,
                                                    yesterday.isoformat(),
                                                    login_links_token.hex, 1)

        assert not stub_mail.called
        assert task_result.state.is_skipped
        task_result.state = m.TaskResultState.not_started
        session.commit()

    with describe('Does nothing when task was already finished'):
        task_result.state = m.TaskResultState.finished
        session.commit()

        psef.tasks._send_login_links_to_users_1(assig_id, task_result.id.hex,
                                                yesterday.isoformat(),
                                                login_links_token.hex, 1)
        assert not stub_mail.called
        assert task_result.state.is_finished
Example #16
0
def test_get_all_comments_of_user(logged_in, test_client, session, admin_user,
                                  describe, tomorrow, make_add_reply):
    with describe('setup'), logged_in(admin_user):
        assignment = helpers.create_assignment(test_client,
                                               state='open',
                                               deadline=tomorrow)
        course = assignment['course']
        teacher1 = admin_user
        teacher2 = helpers.create_user_with_role(session, 'Teacher', course)
        student1 = helpers.create_user_with_role(session, 'Student', course)
        student2 = helpers.create_user_with_role(session, 'Student', course)

        sub1 = helpers.create_submission(test_client,
                                         assignment,
                                         for_user=student1)

        sub2 = helpers.create_submission(test_client,
                                         assignment,
                                         for_user=student2)

        add_reply1 = make_add_reply(sub1)
        add_reply2 = make_add_reply(sub2)

        with logged_in(teacher1):
            rep1_1 = add_reply1('Hello a reply', line=1)
            rep1_2 = add_reply1('Hello a reply', line=1)
            rep2 = add_reply1('Hello a reply', line=10)
            rep3 = add_reply2('Hello a reply', line=5)

        def get_url(user):
            return (f'/api/v1/assignments/{get_id(assignment)}/users'
                    f'/{get_id(user)}/comments/')

    with describe('Can get all comments by a user'), logged_in(teacher1):
        result = [
            {
                '__allow_extra__':
                True,
                'replies': [
                    dict_without(rep1_1, 'author'),
                    dict_without(rep1_2, 'author')
                ],
                'line':
                1,
            },
            {
                '__allow_extra__': True,
                'replies': [dict_without(rep2, 'author')],
                'line': 10,
            },
            {
                '__allow_extra__': True,
                'replies': [dict_without(rep3, 'author')],
                'line': 5,
            },
        ]
        test_client.req('get', get_url(teacher1), 200, result)

    with describe('Does not get threads without own reply'):
        with logged_in(teacher2):
            # rep1_3 is allowed to exist in the reply, however this is not the
            # case with the current implementation. rep5 should never be in the
            # reply.
            rep1_3 = add_reply1('A reply by somebody else', line=1)
            rep5 = add_reply1('A reply by somebody else', line=100)
            test_client.req(
                'get',
                get_url(teacher2),
                200,
                [
                    {
                        '__allow_extra__':
                        True,
                        'replies': [
                            # rep1_1 and rep1_2 are allowed to be present
                            dict_without(rep1_3, 'author')
                        ],
                        'line':
                        1,
                    },
                    {
                        '__allow_extra__': True,
                        'replies': [dict_without(rep5, 'author')],
                        'line': 100,
                    },
                ],
            )

        with logged_in(teacher1):
            test_client.req('get', get_url(teacher1), 200, result)
Example #17
0
def test_reply(logged_in, test_client, session, admin_user, mail_functions,
               describe, tomorrow, make_add_reply):

    with describe('setup'), logged_in(admin_user):
        assignment = helpers.create_assignment(test_client,
                                               state='open',
                                               deadline=tomorrow)
        course = assignment['course']
        teacher = admin_user
        student = helpers.create_user_with_role(session, 'Student', course)

        work_id = helpers.get_id(
            helpers.create_submission(test_client,
                                      assignment,
                                      for_user=student))

        add_reply = make_add_reply(work_id)

        feedback_url = f'/api/v1/submissions/{work_id}/feedbacks/?with_replies'

    with describe('Teachers can give feedback'), logged_in(teacher):
        _, rv = add_reply('base comment', include_response=True)
        assert 'Warning' not in rv.headers
        assert not mail_functions.any_mails(), (
            'Nobody should be emailed for the first reply')

    with describe('Students may not see feedback by default'), logged_in(
            student):
        test_client.req(
            'get',
            feedback_url,
            200,
            result={
                'general': '',
                'linter': {},
                'authors': [],
                'user': []
            },
        )

    with describe('But students can place comments'), logged_in(student):
        add_reply('A reply')
        # The teacher should be mailed as a reply was posted
        mail_functions.assert_mailed(teacher, amount=1)
        # A direct mail should be send
        assert mail_functions.direct.called
        assert not mail_functions.digest.called

    with describe('Students may see own comments'), logged_in(student):
        test_client.req(
            'get',
            feedback_url,
            200,
            result={
                'general':
                '',
                'linter': {},
                'authors': [student],
                'user': [{
                    '__allow_extra__':
                    True,
                    'replies': [{
                        'comment': 'A reply',
                        '__allow_extra__': True
                    }],
                }],
            },
        )

    with describe('Teacher may see all comments'), logged_in(teacher):
        test_client.req(
            'get',
            feedback_url,
            200,
            result={
                'general':
                '',
                'linter': {},
                'authors': [student, teacher],
                'user': [{
                    '__allow_extra__':
                    True,
                    'replies': [{
                        'comment': 'base comment',
                        '__allow_extra__': True
                    }, {
                        'comment': 'A reply',
                        '__allow_extra__': True
                    }],
                }],
            },
        )

    with describe('A teacher can reply and should get a warning'), logged_in(
            teacher):
        _, rv = add_reply('another comment', include_response=True)
        assert 'Warning' in rv.headers
        assert 'have sufficient permissions' in rv.headers['Warning']
Example #18
0
def test_edit_feedback(logged_in, test_client, session, admin_user,
                       mail_functions, describe, tomorrow, make_add_reply):
    with describe('setup'), logged_in(admin_user):
        assignment = helpers.create_assignment(test_client,
                                               state='open',
                                               deadline=tomorrow)
        course = assignment['course']
        teacher = admin_user
        student = helpers.create_user_with_role(session, 'Student', course)
        ta = helpers.create_user_with_role(session, 'TA', course)

        work_id = helpers.get_id(
            helpers.create_submission(test_client,
                                      assignment,
                                      for_user=student))

        add_reply = make_add_reply(work_id)
        feedback_url = f'/api/v1/submissions/{work_id}/feedbacks/?with_replies'
        with logged_in(student):
            reply = add_reply('a reply')
        reply_url = (f'/api/v1/comments/{reply["comment_base_id"]}/'
                     f'replies/{reply["id"]}')

    with describe('Editing with the same content does not create an edit'
                  ), logged_in(teacher):
        test_client.req('patch', reply_url, 200, data={'comment': 'a reply'})
        test_client.req(
            'get',
            feedback_url,
            200,
            result={
                '__allow_extra__':
                True,
                'user': [{
                    '__allow_extra__':
                    True,
                    'replies': [{
                        'id': reply['id'],
                        'comment': 'a reply',
                        'last_edit': None,
                        '__allow_extra__': True,
                    }],
                }]
            },
        )
    with describe('teachers and own user can edit reply'):
        with logged_in(student):
            test_client.req('patch',
                            reply_url,
                            200,
                            data={'comment': 'updated1'})
        with logged_in(teacher):
            test_client.req('patch',
                            reply_url,
                            200,
                            data={'comment': 'updated2'})
        with logged_in(ta):
            test_client.req('patch',
                            reply_url,
                            403,
                            data={'comment': 'updated3'})

        with logged_in(teacher):
            test_client.req(
                'get',
                feedback_url,
                200,
                result={
                    'general':
                    '',
                    'linter': {},
                    'authors': [student],
                    'user': [{
                        '__allow_extra__':
                        True,
                        'replies': [{
                            'id': reply['id'],
                            'comment': 'updated2',
                            'last_edit': str,
                            '__allow_extra__': True,
                        }],
                    }]
                },
            )

    with describe('Students can only see own edits'):
        with logged_in(teacher):
            other_reply = add_reply('A reply jooo')
            other_reply_url = (
                f'/api/v1/comments/{other_reply["comment_base_id"]}/'
                f'replies/{other_reply["id"]}')

        with logged_in(student):
            edits = test_client.req('get',
                                    reply_url + '/edits/',
                                    200,
                                    result=[
                                        {
                                            'id': int,
                                            'old_text': 'updated1',
                                            'created_at': str,
                                            'new_text': 'updated2',
                                            'editor': teacher,
                                        },
                                        {
                                            'id': int,
                                            'old_text': 'a reply',
                                            'created_at': str,
                                            'new_text': 'updated1',
                                            'editor': student,
                                        },
                                    ])

            test_client.req('get', other_reply_url + '/edits/', 403)

    with describe('teachers can see others edits'), logged_in(teacher):
        test_client.req('get', reply_url + '/edits/', 200, result=edits)
        test_client.req('get', other_reply_url + '/edits/', 200, result=[])

    with describe('last_edit should really be the last edit'), logged_in(
            student):
        last_edit = test_client.req('get', feedback_url,
                                    200)['user'][0]['replies'][0]['last_edit']

        now = cg_dt_utils.DatetimeWithTimezone.utcnow()
        with freeze_time(now):
            test_client.req('patch',
                            reply_url,
                            200,
                            data={'comment': 'updated4'})

        new_last_edit = test_client.req(
            'get', feedback_url, 200)['user'][0]['replies'][0]['last_edit']
        assert new_last_edit > last_edit
        assert new_last_edit == now.isoformat()
Example #19
0
def test_division(
    test_client, admin_user, session, describe, logged_in, yesterday, iteration
):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assignment = helpers.create_assignment(
            test_client, course, deadline=yesterday
        )
        helpers.enable_peer_feedback(test_client, assignment)
        user1, user2, user3, user4 = [
            helpers.get_id(
                helpers.create_user_with_role(session, 'Student', course)
            ) for _ in range(4)
        ]

    with describe('Single submission no connections'), logged_in(admin_user):
        assert get_all_connections(assignment, 1) == {}
        helpers.create_submission(test_client, assignment, for_user=user1)
        assert get_all_connections(assignment, 1) == {}

    with describe('Second submission does initial division'
                  ), logged_in(admin_user):
        helpers.create_submission(test_client, assignment, for_user=user2)
        assert get_all_connections(assignment,
                                   1) == {user1: [user2], user2: [user1]}

    with describe('Third submission also gets divided'), logged_in(admin_user):
        helpers.create_submission(test_client, assignment, for_user=user3)
        connections = get_all_connections(assignment, 1)
        assert len(connections) == 3
        if connections[user3] == [user1]:
            assert connections[user2] == [user3]
            assert connections[user1] == [user2]
        else:
            assert connections[user3] == [user2]
            assert connections[user1] == [user3]
            assert connections[user2] == [user1]

    with describe('Submitting again does not change division'
                  ), logged_in(admin_user):
        old_connections = get_all_connections(assignment, 1)
        for _ in range(10):
            last_sub3 = helpers.create_submission(
                test_client, assignment, for_user=user3
            )
            assert get_all_connections(assignment, 1) == old_connections

    with describe('Deleting not last submission does not change division'
                  ), logged_in(admin_user):
        old_connections = get_all_connections(assignment, 1)
        _, rv = test_client.req(
            'delete',
            f'/api/v1/submissions/{helpers.get_id(last_sub3)}',
            204,
            include_response=True,
        )
        assert 'warning' not in rv.headers
        assert get_all_connections(assignment, 1) == old_connections

    with describe('Test submission does not change anything'
                  ), logged_in(admin_user):
        old_connections = get_all_connections(assignment, 1)
        for _ in range(10):
            helpers.create_submission(
                test_client, assignment, is_test_submission=True
            )
            assert get_all_connections(assignment, 1) == old_connections

    with describe('user gets assigned to different user every time'
                  ), logged_in(admin_user):
        conns = set()
        for _ in range(40):
            sub = helpers.create_submission(
                test_client, assignment, for_user=user4
            )
            new_conns = get_all_connections(assignment, 1)[user4]
            conns.add(new_conns[0])
            _, rv = test_client.req(
                'delete',
                f'/api/v1/submissions/{helpers.get_id(sub)}',
                204,
                include_response=True,
            )
            assert 'warning' not in rv.headers
        assert len(conns) == 3
Example #20
0
def test_get_entire_workspace(logged_in, test_client, session, admin_user,
                              describe, tomorrow):
    with describe('setup'), logged_in(admin_user):
        assignment = helpers.create_assignment(test_client,
                                               state='open',
                                               deadline=tomorrow)
        course = assignment['course']
        teacher = admin_user
        student = helpers.create_user_with_role(session, 'Student', course)
        w_id, = assignment["analytics_workspace_ids"]

        work_id = helpers.get_id(
            helpers.create_submission(test_client,
                                      assignment,
                                      for_user=student))

        url = f'/api/v1/analytics/{w_id}'

        rubric = test_client.req(
            'put',
            f'/api/v1/assignments/{get_id(assignment)}/rubrics/',
            200,
            data=RUBRIC)

        def test(*args):
            if not isinstance(args[0], tuple):
                args = [(student, work_id, *args)]

            test_client.req('get',
                            url,
                            200,
                            result={
                                'id': w_id,
                                'assignment_id': assignment['id'],
                                'student_submissions': {
                                    str(get_id(arg[0])): [{
                                        'id':
                                        get_id(arg[1]),
                                        'created_at':
                                        str,
                                        'grade':
                                        arg[2],
                                        'assignee_id':
                                        arg[3] and get_id(arg[3]),
                                    }]
                                    for arg in args
                                },
                                'data_sources': [str, str],
                            })

    with describe('should send the current grade'), logged_in(teacher):
        test(None, None)

        for item in [
                get_rubric_item(rubric, 'My header', '10points'),
                get_rubric_item(rubric, 'My header2', '2points'),
        ]:
            test_client.req(
                'patch',
                f'/api/v1/submissions/{work_id}/rubricitems/{item["id"]}',
                204,
            )

        test(10, None)

        test_client.req('patch',
                        f'/api/v1/submissions/{work_id}',
                        200,
                        data={'grade': 0.5})
        test(0.5, None)

        test_client.req('patch',
                        f'/api/v1/submissions/{work_id}',
                        200,
                        data={'grade': None})
        test(10, None)

    with describe('should show the assignee'), logged_in(teacher):
        test_client.req('patch',
                        f'/api/v1/submissions/{work_id}/grader',
                        204,
                        data={'user_id': get_id(teacher)})

        test(10, teacher)

    with describe('should not show test students'), logged_in(teacher):
        helpers.create_submission(test_client,
                                  assignment,
                                  is_test_submission=True)

        test(10, teacher)

    with describe('Should show multiple students'), logged_in(teacher):
        student2 = helpers.create_user_with_role(session, 'Student', course)
        work2 = helpers.create_submission(test_client,
                                          assignment,
                                          for_user=student2)
        test((student, work_id, 10, teacher), (student2, work2, None, None))

    with describe('students cannot access it'), logged_in(student):
        test_client.req('get', url, 403)
Example #21
0
def test_delete_feedback(logged_in, test_client, session, admin_user,
                         mail_functions, describe, tomorrow, make_add_reply):
    with describe('setup'), logged_in(admin_user):
        assignment = helpers.create_assignment(test_client,
                                               state='open',
                                               deadline=tomorrow)
        course = assignment['course']
        teacher = admin_user
        student = helpers.create_user_with_role(session, 'Student', course)
        ta = helpers.create_user_with_role(session, 'TA', course)

        work_id = helpers.get_id(
            helpers.create_submission(test_client,
                                      assignment,
                                      for_user=student))

        add_reply = make_add_reply(work_id)
        feedback_url = f'/api/v1/submissions/{work_id}/feedbacks/?with_replies'

    with describe('Users can delete own replies'):
        for user in [teacher, student, ta]:
            with logged_in(user):
                reply = add_reply('a reply')
                test_client.req(
                    'delete', (f'/api/v1/comments/{reply["comment_base_id"]}/'
                               f'replies/{reply["id"]}'), 204)
                with logged_in(teacher):
                    test_client.req(
                        'get',
                        feedback_url,
                        200,
                        result={
                            'general': '',
                            'linter': {},
                            'authors': [],
                            'user': []
                        },
                    )

    with describe('Only teachers can delete replies of others'):
        with logged_in(student):
            reply, base = add_reply('a reply', include_base=True)
        with logged_in(ta):
            test_client.req('delete',
                            (f'/api/v1/comments/{reply["comment_base_id"]}/'
                             f'replies/{reply["id"]}'), 403)
        with logged_in(teacher):
            test_client.req(
                'get',
                feedback_url,
                200,
                result={
                    'general': '',
                    'linter': {},
                    'authors': [student],
                    'user': [base],
                },
            )

        with logged_in(teacher):
            test_client.req('delete',
                            (f'/api/v1/comments/{reply["comment_base_id"]}/'
                             f'replies/{reply["id"]}'), 204)
            test_client.req(
                'get',
                feedback_url,
                200,
                result={
                    'general': '',
                    'linter': {},
                    'authors': [],
                    'user': []
                },
            )

    with describe('Cannot delete a reply twice'), logged_in(student):
        reply = add_reply('To delete')
        test_client.req('delete',
                        (f'/api/v1/comments/{reply["comment_base_id"]}/'
                         f'replies/{reply["id"]}'), 204)
        test_client.req('delete',
                        (f'/api/v1/comments/{reply["comment_base_id"]}/'
                         f'replies/{reply["id"]}'), 404)
Example #22
0
def test_getting_inline_feedback_analytics(logged_in, test_client, session,
                                           admin_user, describe, tomorrow,
                                           make_add_reply):
    with describe('setup'), logged_in(admin_user):
        assignment = helpers.create_assignment(test_client,
                                               state='open',
                                               deadline=tomorrow)
        course = assignment['course']
        teacher = admin_user
        student = helpers.create_user_with_role(session, 'Student', course)

        work_id = helpers.get_id(
            helpers.create_submission(test_client,
                                      assignment,
                                      for_user=student))

        add_reply = make_add_reply(work_id)
        base_url = (
            f'/api/v1/analytics/{assignment["analytics_workspace_ids"][0]}')
        url = base_url + '/data_sources/inline_feedback'

        def test(amount):
            test_client.req(
                'get',
                url,
                200,
                result={
                    'name': 'inline_feedback',
                    'data': {
                        str(work_id): {
                            'total_amount': amount,
                        }
                    },
                },
            )

    with describe('should be includes as data source'), logged_in(teacher):
        test_client.req('get',
                        base_url,
                        200,
                        result={
                            'id': assignment['analytics_workspace_ids'][0],
                            'assignment_id': assignment['id'],
                            'student_submissions': {
                                str(get_id(student)): [{
                                    'id': work_id,
                                    'created_at': str,
                                    'grade': None,
                                    'assignee_id': None,
                                }],
                            },
                            'data_sources': lambda x: 'inline_feedback' in x,
                        })

    with describe('standard no comments'), logged_in(teacher):
        test(0)

    with describe('after adding reply it should return 1'), logged_in(teacher):
        r1 = add_reply('A reply', line=1)
        test(1)

    with describe('after two replies on 1 line should return 1'), logged_in(
            teacher):
        r2 = add_reply('A reply on the reply', line=1)
        test(1)

    with describe('a reply on another line should return more'), logged_in(
            teacher):
        r3 = add_reply('A new line reply', line=2)
        r4 = add_reply('WOW MUCH REPLIES', line=1000)
        test(3)

    with describe('Updating should not change a thing'), logged_in(teacher):
        test(3)
        r4 = r4.update('wow new text')
        test(3)

    with describe('after deleting replies it should be back at 0'), logged_in(
            teacher):
        # Should begin with 3
        test(3)

        # There is another comment on this line so it should stay the same
        r1.delete()
        test(3)
        r3.delete()
        test(2)
        r4.delete()
        test(1)
        r2.delete()
        test(0)

    with describe('students cannot access it'), logged_in(student):
        test_client.req('get', url, 403)
Example #23
0
def test_getting_analytics_workspace(logged_in, test_client, session,
                                     admin_user, describe, tomorrow):
    with describe('setup'), logged_in(admin_user):
        assignment = helpers.create_assignment(test_client,
                                               state='open',
                                               deadline=tomorrow)
        course = assignment['course']
        teacher = admin_user
        student = helpers.create_user_with_role(session, 'Student', course)

        work_id = helpers.get_id(
            helpers.create_submission(test_client,
                                      assignment,
                                      for_user=student))

        base_url = (
            f'/api/v1/analytics/{assignment["analytics_workspace_ids"][0]}')
        url = base_url + '/data_sources/rubric_data'

        def test(items):
            test_client.req(
                'get',
                url,
                200,
                result={
                    'name': 'rubric_data',
                    'data': {
                        str(work_id): [{
                            'item_id': get_id(item),
                            'multiplier': 1.0,
                        } for item in items]
                    },
                },
            )

    with describe('by default there should be not rubric data source'
                  ), logged_in(teacher):
        test_client.req('get',
                        base_url,
                        200,
                        result={
                            'id': assignment['analytics_workspace_ids'][0],
                            'assignment_id': assignment['id'],
                            'student_submissions': {
                                str(get_id(student)): [{
                                    'id': work_id,
                                    'created_at': str,
                                    'grade': None,
                                    'assignee_id': None,
                                }],
                            },
                            'data_sources': lambda x: 'rubric_data' not in x,
                        })
        test_client.req('get', url, 404)

    with describe('After adding a rubric it should have the data source'
                  ), logged_in(teacher):
        rubric = test_client.req(
            'put',
            f'/api/v1/assignments/{get_id(assignment)}/rubrics/',
            200,
            data=RUBRIC)
        test_client.req('get',
                        base_url,
                        200,
                        result={
                            'id': assignment['analytics_workspace_ids'][0],
                            'assignment_id': assignment['id'],
                            'student_submissions': {
                                str(get_id(student)): [{
                                    'id': work_id,
                                    'created_at': str,
                                    'grade': None,
                                    'assignee_id': None,
                                }],
                            },
                            'data_sources': lambda x: 'rubric_data' in x,
                        })
        test([])

    with describe('After selecting there are returned'), logged_in(teacher):
        to_select = [
            get_rubric_item(rubric, 'My header', '10points'),
            get_rubric_item(rubric, 'My header2', '2points'),
        ]

        for idx, item in enumerate(to_select):
            test_client.req(
                'patch',
                f'/api/v1/submissions/{work_id}/rubricitems/{item["id"]}',
                204,
            )

            test(to_select[:idx + 1])

    with describe('Changing item in header works'), logged_in(teacher):
        item = get_rubric_item(rubric, 'My header', '5points')
        to_select[0] = item
        test_client.req(
            'patch',
            f'/api/v1/submissions/{work_id}/rubricitems/{item["id"]}',
            204,
        )

        test(to_select)

    with describe('students cannot access it'), logged_in(student):
        test_client.req('get', url, 403)
Example #24
0
def test_git_in_groups(basic, describe, logged_in, session, test_client,
                       monkeypatch, stub_function_class):
    with describe('setup'):
        course, assig, teacher, student = basic
        student2 = helpers.create_user_with_role(session, 'Student', [course])
        stub_clone = stub_function_class()
        monkeypatch.setattr(p.tasks, 'clone_commit_as_submission', stub_clone)
        url = f'/api/v1/assignments/{assig.id}'

        with logged_in(student):
            student1_webhook_id = test_client.req(
                'post', f'{url}/webhook_settings?webhook_type=git', 200)['id']

        with logged_in(teacher):
            group_set = helpers.create_group_set(test_client, course, 1, 2,
                                                 [assig])
            group = m.Group.query.get(
                helpers.create_group(test_client, group_set, [student2])['id'])

        with logged_in(student2):
            group_webhook_id = test_client.req(
                'post', f'{url}/webhook_settings?webhook_type=git', 200)['id']

    def do_request(webhook_id, user_id, status=200):
        data = get_request_data('github', 'push')
        kwargs = {'content_type': 'application/json'}
        webhook = m.WebhookBase.query.get(webhook_id)
        assert webhook is not None
        assert user_id == webhook.user_id

        signature = hmac.new(webhook.secret.encode(), data.encode(),
                             'sha1').hexdigest()

        return test_client.req(
            'post',
            f'/api/v1/webhooks/{webhook_id}',
            status,
            real_data=data,
            headers={
                'X-GitHub-Delivery': str(uuid.uuid4()),
                'X-GitHub-Event': 'push',
                'X-Hub-Signature': f'sha1={signature}',
            },
            **kwargs,
        )

    with describe('It should work for users not in a group'):
        do_request(student1_webhook_id, student.id)
        assert stub_clone.called

    with describe('It should work for users in a group'):
        do_request(group_webhook_id, group.virtual_user.id)
        assert stub_clone.called

    with describe('After joining group old webhook stops working'):
        with logged_in(student):
            _, rv = test_client.req('post',
                                    f'/api/v1/groups/{group.id}/member',
                                    200,
                                    data={'username': student.username},
                                    include_response=True)
            assert 'warning' in rv.headers
            assert 'existing webhook for ' in rv.headers['warning']

        do_request(student1_webhook_id, student.id, status=400)
        assert not stub_clone.called
Example #25
0
def test_webhooks_group_join_lti(describe, logged_in, session, test_client,
                                 monkeypatch, stub_function_class, admin_user,
                                 app):
    with describe('setup'):
        course = helpers.create_lti_course(session, app)
        assig = helpers.create_lti_assignment(session, course, state='open')
        teacher = helpers.create_user_with_role(session, 'Teacher', [course])
        student1 = helpers.create_user_with_role(session, 'Student', [course])
        student2 = helpers.create_user_with_role(session, 'Student', [course])
        url = f'/api/v1/assignments/{assig.id}'

        with logged_in(teacher):
            group_set = helpers.create_group_set(test_client, course, 1, 2,
                                                 [assig])
            group = m.Group.query.get(
                helpers.create_group(test_client, group_set, [])['id'])
            test_client.req(
                'patch',
                url,
                200,
                data={
                    'files_upload_enabled': True,
                    'webhook_upload_enabled': True,
                },
            )

    with describe('Can join group without webhook'):
        with logged_in(student2):
            test_client.req(
                'post',
                f'/api/v1/groups/{group.id}/member',
                200,
                data={'username': student2.username},
            )

    with describe('Cannot make webhook with unfinished group'):
        with logged_in(student2):
            test_client.req('post', f'{url}/webhook_settings?webhook_type=git',
                            400)

        session.add(
            m.AssignmentResult(
                user_id=student2.id,
                assignment_id=assig.id,
                sourcedid=str(uuid.uuid4()),
            ))
        session.commit()

        with logged_in(student2):
            group_webhook = test_client.req(
                'post', f'{url}/webhook_settings?webhook_type=git', 200)

    with describe(
            'Cannot join group as it has a webhook but user does not have sourcedid'
    ):
        with logged_in(student1):
            test_client.req(
                'post',
                f'/api/v1/groups/{group.id}/member',
                400,
                data={'username': student1.username},
            )

        with logged_in(student1):
            # Cannot create individual webhook
            test_client.req('post', f'{url}/webhook_settings?webhook_type=git',
                            400)

        session.add(
            m.AssignmentResult(
                user_id=student1.id,
                assignment_id=assig.id,
                sourcedid=str(uuid.uuid4()),
            ))
        session.commit()

        with logged_in(student1):
            _, rv = test_client.req(
                'post',
                f'/api/v1/groups/{group.id}/member',
                200,
                data={'username': student1.username},
                include_response=True,
            )
            assert 'warning' not in rv.headers

        with logged_in(student1):
            # And we now get the group webhook config.
            test_client.req('post',
                            f'{url}/webhook_settings?webhook_type=git',
                            200,
                            result=group_webhook)
Example #26
0
def test_getting_files_from_proxy(test_client, logged_in, describe, session,
                                  admin_user, monkeypatch):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assig = helpers.create_assignment(test_client,
                                          course,
                                          state='open',
                                          deadline='tomorrow')
        stud = helpers.create_user_with_role(session, 'Student', [course])

        sub_data = (
            (f'{os.path.dirname(__file__)}/../test_data/test_submissions/'
             'html.tar.gz'),
            'f.tar.gz',
        )
        s1 = helpers.create_submission(test_client,
                                       assig,
                                       submission_data=sub_data,
                                       for_user=stud)['id']

    with describe('Proxy allows views without active user'):
        with logged_in(stud):
            proxy = test_client.req('post',
                                    f'/api/v1/submissions/{s1}/proxy',
                                    200,
                                    data={
                                        'allow_remote_resources': True,
                                        'allow_remote_scripts': True,
                                        'teacher_revision': False,
                                    })['id']

        res = test_client.post(f'/api/v1/proxies/{proxy}/nested/index.html',
                               follow_redirects=False)
        assert res.status_code == 303
        res = test_client.get(res.headers['Location'])
        assert res.headers['Content-Type'] == 'text/html; charset=utf-8'
        assert res.headers[
            'Content-Security-Policy'] == "default-src * data: 'unsafe-eval' 'unsafe-inline'"

    with describe('Proxy should have correct content type'):
        res = test_client.get(f'/api/v1/proxies/fiets.jpg')
        assert res.status_code == 200
        assert res.headers['Content-Type'] == 'image/jpeg'

    with describe('Not found files should return a 404'):
        for f in ['nope', 'non_existing_dir/file', 'nested/nope']:
            res = test_client.get(f'/api/v1/proxies/{f}')
            assert res.status_code == 404

    with describe('A directory will try to retrieve the index.html'):
        res1 = test_client.get(f'/api/v1/proxies/nested/index.html')
        assert res1.status_code == 200

        res2 = test_client.get(f'/api/v1/proxies/nested')
        assert res2.status_code == 200

        res3 = test_client.get(f'/api/v1/proxies/nested')
        assert res3.status_code == 200

        assert len(res1.get_data()) > 1
        assert res1.get_data() == res2.get_data()
        assert res1.get_data() == res3.get_data()

    with describe('A path is required'):
        res = test_client.get(f'/api/v1/proxies/')
        assert res.status_code == 404

    with describe('When we find a dir 404 is returned'):
        monkeypatch.setattr(psef.v1.proxies, '_INDEX_FILES',
                            ['not_in_dir.html'])
        res = test_client.get(f'/api/v1/proxies/nested')
        assert res.status_code == 404

    with describe('A proxy should stop working when expired'):
        url = f'/api/v1/proxies/fiets.jpg'
        assert test_client.get(url).status_code == 200

        with freeze_time(datetime.utcnow() + timedelta(days=1)):
            assert test_client.get(url).status_code == 400

    with describe('A proxy cannot be used when the stat is deleted'):
        url = f'/api/v1/proxies/fiets.jpg'
        assert test_client.get(url).status_code == 200

        psef.models.Proxy.query.filter_by(id=proxy).update({
            'state':
            psef.models.ProxyState.deleted,
        })
        assert test_client.get(url).status_code == 404

        psef.models.Proxy.query.filter_by(id=proxy).update({
            'state':
            psef.models.ProxyState.in_use,
        })
        assert test_client.get(url).status_code == 200

    with describe('We cannot reused the proxy'):
        assert test_client.get('/api/v1/proxies/fiets.jpg').status_code == 200
        assert test_client.get(
            f'/api/v1/proxies/{proxy}/fiets.jpg').status_code == 200

        # After clearing cookies we cannot use the endpoint anymore
        test_client.cookie_jar.clear()
        assert test_client.get('/api/v1/proxies/fiets.jpg').status_code == 400
        assert test_client.get(
            f'/api/v1/proxies/{proxy}/fiets.jpg').status_code == 400

        # Cannot do a new post to the proxy endpoint
        res = test_client.post(f'/api/v1/proxies/{proxy}/nested/index.html',
                               follow_redirects=True)
        assert res.status_code == 404
        assert test_client.get('/api/v1/proxies/fiets.jpg').status_code == 400
Example #27
0
def test_run_auto_test(monkeypatch_celery, monkeypatch_broker, basic,
                       test_client, logged_in, describe, live_server, lxc_stub,
                       monkeypatch, app, session, stub_function_class,
                       assert_similar, monkeypatch_for_run):
    tmpdir = None

    with describe('setup'):
        course, assig_id, teacher, student1 = basic
        student2 = helpers.create_user_with_role(session, 'Student', [course])

        with logged_in(teacher):
            test = helpers.create_auto_test(
                test_client,
                assig_id,
                amount_sets=2,
                amount_suites=2,
                amount_fixtures=1,
                stop_points=[0.5, None],
                grade_calculation='partial',
            )
            test_client.req('patch',
                            f'/api/v1/assignments/{assig_id}',
                            200,
                            data={'state': 'open'})
        url = f'/api/v1/auto_tests/{test["id"]}'

        _run_student = psef.auto_test._run_student

        monkeypatch.setattr(psef.auto_test, '_get_home_dir',
                            stub_function_class(lambda: tmpdir))

        def next_student(*args, **kwargs):
            nonlocal tmpdir

            with tempfile.TemporaryDirectory() as tdir:
                tmpdir = tdir
                for inner in ['/student', '/fixtures']:
                    shutil.copytree(upper_tmpdir + inner, tdir + inner)
                return _run_student(*args, **kwargs)

        monkeypatch.setattr(psef.auto_test, '_run_student', next_student)

        with logged_in(student1):
            work1 = helpers.create_submission(test_client, assig_id)
            with logged_in(teacher):
                test_client.req('patch',
                                f'/api/v1/submissions/{work1["id"]}',
                                200,
                                data={'grade': 5.0})
        with logged_in(student2):
            prog = """
            import sys
            if len(sys.argv) > 1:
                print("hello {}!".format(sys.argv[1]))
            else:
                print("hello stranger!")
            """
            work2 = helpers.create_submission(
                test_client,
                assig_id,
                submission_data=(io.BytesIO(dedent(prog).encode()), 'test.py'))

    with describe(
            'start_auto_test'), tempfile.TemporaryDirectory() as upper_tmpdir:
        tmpdir = upper_tmpdir

        t = m.AutoTest.query.get(test['id'])
        with logged_in(teacher):
            test_client.req('post',
                            f'{url}/runs/',
                            200,
                            data={
                                'continuous_feedback_run': False,
                            })
            session.commit()
        run = t.test_run

        live_server_url = live_server()
        psef.auto_test.start_polling(app.config, repeat=False)

        with logged_in(teacher, yield_token=True) as token:
            response = requests.get(
                f'{live_server_url}{url}/runs/{run.id}',
                headers={'Authorization': f'Bearer {token}'})
            response.raise_for_status()
            assert_similar(
                response.json(), {
                    'id': run.id,
                    'created_at': str,
                    'state': 'done',
                    'results': [object, object],
                    'setup_stderr': str,
                    'setup_stdout': str,
                    'is_continuous': False,
                })

        res1 = session.query(
            m.AutoTestResult).filter_by(work_id=work1['id']).one().id
        res2 = session.query(
            m.AutoTestResult).filter_by(work_id=work2['id']).one().id

        with logged_in(student1):
            # A student cannot see results before the assignment is done
            test_client.req('get', f'{url}/runs/{run.id}/results/{res1}', 403)

        with logged_in(teacher):
            # A teacher can see the results before done
            test_client.req('get', f'{url}/runs/{run.id}/results/{res1}', 200)
            test_client.req('patch',
                            f'/api/v1/assignments/{assig_id}',
                            200,
                            data={'state': 'done'})

        with logged_in(student1):
            # A student cannot see the results of another student
            test_client.req('get', f'{url}/runs/{run.id}/results/{res2}', 403)

            # You should be able too see your own results
            res = test_client.req('get',
                                  f'{url}/runs/{run.id}/results/{res1}',
                                  200,
                                  result={
                                      'id': int,
                                      'points_achieved': 0,
                                      '__allow_extra__': True
                                  })
            # There should be only two results as all other tests should be
            # skipped
            len(res['step_results']) == 2
            test_client.req('get',
                            f'/api/v1/submissions/{work1["id"]}',
                            200,
                            result={
                                '__allow_extra__': True,
                                'grade': 5.0,
                                'grade_overridden': True,
                            })

        with logged_in(student2):
            # You should be able too see your own results
            res = test_client.req(
                'get',
                f'{url}/runs/{run.id}/results/{res2}',
                200,
                result={
                    # The last two run programs should time out, so 2 * 3 + 2 *
                    # (3 - 1) = 10 points achieved
                    'id': int,
                    'points_achieved': 10,
                    'step_results': list,
                    '__allow_extra__': True,
                })
            # Nothing should be skipped, so all steps added
            len(res['step_results']) == 4 * 2 * 2
            test_client.req('get',
                            f'/api/v1/submissions/{work2["id"]}',
                            200,
                            result={
                                '__allow_extra__': True,
                                'grade': 10.0,
                            })

            # Now you should not be able to see step details anymore
            for step in m.AutoTestStepBase.query:
                step.hidden = True
            session.commit()
            old_step_results = res['step_results']
            res = test_client.req('get', f'{url}/runs/{run.id}/results/{res2}',
                                  200)
            for step_res1, step_res2 in zip(res['step_results'],
                                            old_step_results):
                assert step_res1['id'] == step_res2['id']

                assert step_res1['auto_test_step']['hidden']
                assert not step_res2['auto_test_step']['hidden']

                if step_res1['achieved_points']:
                    assert step_res1['log'] != step_res2['log']
                else:
                    assert step_res1['log'] == {}

    with describe('delete result'):
        with logged_in(student1):
            test_client.req('delete', f'{url}/runs/{run.id}', 403)
        with logged_in(teacher):
            test_client.req('delete', f'{url}/runs/{run.id}', 204)
            test_client.req('get', f'{url}/runs/{run.id}', 404)
            test_client.req('get',
                            f'/api/v1/submissions/{work2["id"]}',
                            200,
                            result={
                                '__allow_extra__': True,
                                'grade': None,
                            })
Example #28
0
def test_enabling_login_links(
    describe, test_client, logged_in, admin_user, yesterday, session,
    stub_function, app, monkeypatch, tomorrow, error_template
):
    with describe('setup'), logged_in(admin_user):
        send_mail_stub = stub_function(psef.mail, 'send_login_link_mail')
        assig = helpers.create_assignment(test_client)
        assig_id = helpers.get_id(assig)
        course = helpers.get_id(assig['course'])
        url = f'/api/v1/assignments/{assig_id}?no_course_in_assignment=t'
        teacher = helpers.create_user_with_perms(
            session, [
                CPerm.can_see_assignments,
                CPerm.can_see_hidden_assignments,
                CPerm.can_view_analytics,
                CPerm.can_edit_assignment_info,
            ], course
        )
        no_perm = helpers.create_user_with_perms(
            session, [
                CPerm.can_see_assignments,
                CPerm.can_see_hidden_assignments,
                CPerm.can_view_analytics,
            ], course
        )
        # Make sure there are users to email
        for _ in range(10):
            helpers.create_user_with_role(session, 'Student', course)

    with describe('cannot enable login links for wrong kind'
                  ), logged_in(teacher):
        test_client.req('patch', url, 409, data={'send_login_links': True})
        _, rv = test_client.req(
            'patch',
            url,
            200,
            data={
                'deadline': yesterday.isoformat(),
                'available_at': (yesterday - timedelta(minutes=1)).isoformat(),
                'kind': 'exam',
                'send_login_links': True,
            },
            result={
                '__allow_extra__': True,
                'send_login_links': True,
            },
            include_response=True,
        )
        warning = rv.headers['warning']
        assert re.match(r'.*deadline.*expired.*not send', warning)
        assert not send_mail_stub.called

    with describe('cannot enable login links for wrong kind'
                  ), logged_in(teacher):
        test_client.req('patch', url, 409, data={'kind': 'normal'})
        assert not send_mail_stub.called

    with describe('cannot change login links with incorrect perms'
                  ), logged_in(no_perm):
        test_client.req('patch', url, 403, data={'send_login_links': False})
        assert not send_mail_stub.called

    with describe('Setting again does nothing'), logged_in(teacher):
        old_token = m.Assignment.query.get(assig_id).send_login_links_token
        test_client.req(
            'patch',
            url,
            200,
            data={
                'send_login_links': True,
                'available_at': (yesterday - timedelta(minutes=1)).isoformat(),
            }
        )
        new_token = m.Assignment.query.get(assig_id).send_login_links_token
        assert new_token == old_token
        assert not send_mail_stub.called

    with describe('Changing available at reschedules links'
                  ), logged_in(teacher):
        old_token = m.Assignment.query.get(assig_id).send_login_links_token
        test_client.req(
            'patch',
            url,
            200,
            data={
                'send_login_links': True,
                'available_at': (yesterday - timedelta(minutes=2)).isoformat(),
            }
        )
        new_token = m.Assignment.query.get(assig_id).send_login_links_token
        assert new_token != old_token
        assert new_token is not None
        assert not send_mail_stub.called

    with describe('Disabling clears token'), logged_in(teacher):
        test_client.req('patch', url, 200, data={'send_login_links': False})
        new_token = m.Assignment.query.get(assig_id).send_login_links_token
        assert new_token is None
        assert not send_mail_stub.called

    with describe('Cannot enable login links with large availability window'
                  ), logged_in(teacher):
        psef.site_settings.Opt.EXAM_LOGIN_MAX_LENGTH.set_and_commit_value(
            'P1D'
        )
        test_client.req(
            'patch',
            url,
            409,
            data={
                'send_login_links': True,
                'deadline': tomorrow.isoformat(),
                'available_at': yesterday.isoformat(),
            },
            result={
                **error_template, 'message':
                    re.compile(
                        'Login links can only be enabled if the deadline is at'
                        ' most 24 hours after.*'
                    )
            }
        )
Example #29
0
def test_sending_login_links(
    app, monkeypatch, describe, test_client, logged_in, admin_user, tomorrow,
    session, stub_function, error_template
):
    with describe('setup'), logged_in(admin_user):
        external_url = f'https://{uuid.uuid4()}.com'
        monkeypatch.setitem(app.config, 'EXTERNAL_URL', external_url)
        psef.site_settings.Opt.LOGIN_TOKEN_BEFORE_TIME.set_and_commit_value([
            timedelta(hours=1), timedelta(minutes=5)
        ])
        orig_send_links = psef.tasks.send_login_links_to_users
        stub_send_links = stub_function(
            psef.tasks, 'send_login_links_to_users'
        )
        stub_function(psef.tasks, 'maybe_open_assignment_at')

        other_course = helpers.create_course(test_client)

        course = helpers.create_course(test_client)
        assig = helpers.create_assignment(test_client, course)
        url = f'/api/v1/assignments/{helpers.get_id(assig)}'

        test_client.req(
            'patch',
            url,
            200,
            data={
                'deadline': (tomorrow + timedelta(hours=1)).isoformat(),
                'available_at': tomorrow.isoformat(),
                'kind': 'exam',
            },
        )

        teacher = admin_user
        students = [
            helpers.create_user_with_role(
                session, 'Student', [course, other_course]
            ) for _ in range(10)
        ]

        with logged_in(students[0]):
            test_client.req(
                'get', '/api/v1/courses/', 200,
                [{'__allow_extra__': True, 'id': helpers.get_id(course)},
                 {'__allow_extra__': True, 'id': helpers.get_id(other_course)}]
            )

    with describe('can send link and each user receives a unique link'
                  ), logged_in(teacher):
        test_client.req('patch', url, 200, data={'send_login_links': True})
        assert stub_send_links.called_amount == 2
        # We safe the arguments here as the stub function will be reset at the
        # end of the describe block.
        do_send_links = [
            lambda args=args, kwargs=kwargs: orig_send_links(*args, **kwargs)
            for args, kwargs in
            zip(stub_send_links.args, stub_send_links.kwargs)
        ]

        with psef.mail.mail.record_messages() as outbox:
            with freeze_time(tomorrow - timedelta(hours=1)):
                del flask.g.request_start_time
                do_send_links[0]()

            assert len(outbox) == len(students)
            for mail in outbox:
                assert len(mail.recipients) == 1
            outbox_by_email = {mail.recipients[0]: mail for mail in outbox}
            users_by_link = {}
            link_ids = []

            for student in students:
                student = student._get_current_object()
                mail = outbox_by_email.pop((student.name, student.email))
                match = re.search(
                    rf'a href="({external_url}/[^"]+)"', str(mail)
                )
                link, = match.groups(1)
                assert link.startswith(external_url)
                assert link not in users_by_link
                users_by_link[link] = student
                link_id = link.split('/')[-1]
                link_ids.append((link_id, student))

            assert not outbox_by_email, 'No extra mails should be sent'

    with describe('second email send the same link'), logged_in(teacher):
        with psef.mail.mail.record_messages() as outbox:
            with freeze_time(tomorrow - timedelta(minutes=1)):
                del flask.g.request_start_time
                do_send_links[1]()

            assert len(outbox) == len(students)
            for mail in outbox:
                assert len(mail.recipients) == 1
            outbox_by_email = {mail.recipients[0]: mail for mail in outbox}

            for student in students:
                mail = outbox_by_email.pop((student.name, student.email))
                match = re.search(
                    rf'a href="({external_url}/[^"]+)"', str(mail)
                )
                link, = match.groups(1)
                assert link.startswith(external_url)
                assert users_by_link[link] == student

            assert not outbox_by_email, 'No extra mails should be sent'

    with describe('can see link before available_at, but no login'
                  ), freeze_time(tomorrow - timedelta(hours=1)):
        for link_id, student in link_ids:
            test_client.req(
                'get',
                f'/api/v1/login_links/{link_id}',
                200,
                {
                    'id': link_id,
                    'user': student,
                    # Assignment is already visible
                    'assignment': {
                        '__allow_extra__': True,
                        'id': helpers.get_id(assig),
                        'state': 'hidden',
                    }
                }
            )

            test_client.req(
                'post', f'/api/v1/login_links/{link_id}/login', 409, {
                    **error_template,
                    'message': re.compile('.*wait for an hour'),
                }
            )

    with describe(
        'can login when assignment has started, token is scoped to the course'
    ), freeze_time(tomorrow + timedelta(seconds=15)):
        for idx, (link_id, student) in enumerate(link_ids):
            test_client.req(
                'get',
                f'/api/v1/login_links/{link_id}',
                200,
                {
                    'id': link_id,
                    'user': student,
                    # Assignment is already visible
                    'assignment': {
                        '__allow_extra__': True,
                        'id': helpers.get_id(assig),
                        'state': 'hidden' if idx == 0 else 'submitting',
                    }
                }
            )

            token = test_client.req(
                'post', f'/api/v1/login_links/{link_id}/login', 200, {
                    'user': {
                        '__allow_extra__': True,
                        'id': student.id,
                    },
                    'access_token': str,
                }
            )['access_token']

            # Make sure we are logged in for the correct user.
            headers = {'Authorization': f'Bearer {token}'}
            test_client.req(
                'get',
                '/api/v1/login',
                200,
                headers=headers,
                result={'id': student.id, '__allow_extra__': True},
            )
            test_client.req(
                'get',
                '/api/v1/courses/',
                200,
                headers=headers,
                result=[{
                    'id': helpers.get_id(course), '__allow_extra__': True
                }],
            )
            test_client.req(
                'get',
                f'/api/v1/courses/{helpers.get_id(other_course)}',
                403,
                headers=headers,
            )

    with describe('cannot login if deadline has expired'
                  ), freeze_time(tomorrow + timedelta(hours=2)):
        for idx, (link_id, student) in enumerate(link_ids):
            token = test_client.req(
                'post',
                f'/api/v1/login_links/{link_id}/login',
                400,
                {
                    **error_template,
                    'message':
                        re.compile(
                            'The deadline for this assignment has already'
                            ' expired'
                        ),
                    'code': 'OBJECT_EXPIRED',
                },
            )
Example #30
0
def test_teacher_revision_in_proxy(test_client, logged_in, describe, session,
                                   admin_user, app):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assig = helpers.create_assignment(test_client,
                                          course,
                                          state='done',
                                          deadline='tomorrow')
        stud = helpers.create_user_with_role(session, 'Student', [course])

        sub_data = (
            (f'{os.path.dirname(__file__)}/../test_data/test_submissions/'
             'html.tar.gz'),
            'f.tar.gz',
        )
        s1 = helpers.create_submission(test_client,
                                       assig,
                                       submission_data=sub_data,
                                       for_user=stud)['id']
        files = test_client.req(
            'get',
            f'/api/v1/submissions/{s1}/files/',
            200,
        )

        # Get the file nested/index.html
        student_file_id = [
            n['id'] for f in files['entries'] for n in f.get('entries', [])
            if f['name'] == 'nested' and n['name'] == 'index.html'
        ][0]

        test_client.req(
            'patch',
            f'/api/v1/code/{student_file_id}',
            200,
            real_data='TEACHER FILE',
        )
        test_client.req(
            'post',
            f'/api/v1/submissions/{s1}/files/?path=f.tar.gz/nested/woo',
            200,
        )

        student_proxy = test_client.req('post',
                                        f'/api/v1/submissions/{s1}/proxy',
                                        200,
                                        data={
                                            'allow_remote_resources': True,
                                            'allow_remote_scripts': True,
                                            'teacher_revision': False,
                                        })['id']
        teacher_proxy = test_client.req('post',
                                        f'/api/v1/submissions/{s1}/proxy',
                                        200,
                                        data={
                                            'allow_remote_resources': True,
                                            'allow_remote_scripts': True,
                                            'teacher_revision': True,
                                        })['id']

    with describe(
            'Teacher revision proxy should return teacher rev'
    ), app.test_client() as t_client, app.test_client() as s_client:
        res = t_client.post(
            f'/api/v1/proxies/{teacher_proxy}/nested/index.html',
            follow_redirects=True)
        assert res.status_code == 200
        assert res.get_data() == b'TEACHER FILE'

        res = s_client.post(
            f'/api/v1/proxies/{student_proxy}/nested/index.html',
            follow_redirects=True)
        assert res.status_code == 200
        assert res.get_data() != b'TEACHER FILE'

        res = t_client.get(f'/api/v1/proxies/nested/woo')
        assert res.status_code == 200
        assert res.get_data() == b''

        res = s_client.get(f'/api/v1/proxies/nested/woo')
        assert res.status_code == 404