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)
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)
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']
def test_getting_peer_feedback_connections(test_client, admin_user, session, describe, logged_in, yesterday, tomorrow): with describe('setup'), logged_in(admin_user): course = helpers.create_course(test_client) assignment = helpers.create_assignment(test_client, course, deadline=yesterday, state='open') teacher = helpers.create_user_with_role(session, 'Teacher', course) users = [ helpers.get_id( helpers.create_user_with_role(session, 'Student', course)) for _ in range(5) ] user1, user2, *_ = users for user in users: helpers.create_submission(test_client, assignment, for_user=user) helpers.enable_peer_feedback(test_client, assignment, amount=1) url = (f'/api/v1/assignments/{helpers.get_id(assignment)}/users' f'/{helpers.get_id(user1)}/peer_feedback_subjects/') conns = get_all_connections(assignment, 1)[user1] with describe('Can get connections for own user'): with logged_in(user1): api_conns = test_client.req( 'get', url, 200, result=[{ 'peer': helpers.to_db_object(user1, m.User), 'subject': helpers.to_db_object(conns[0], m.User), }]) with describe('Cannot get connections for other user as student'): with logged_in(user2): test_client.req('get', url, 403) with describe('Can get connections for other user as teacher'): with logged_in(teacher): test_client.req('get', url, 200, result=api_conns) with describe( 'No connections if assignments deadline has not expired just yet'): with logged_in(teacher): test_client.req( 'patch', f'/api/v1/assignments/{helpers.get_id(assignment)}', 200, data={'deadline': tomorrow.isoformat()}) with logged_in(user1): test_client.req('get', url, 200, result=[]) # Not even as teacher with logged_in(teacher): test_client.req('get', url, 200, result=[])
def 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)'
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)
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)
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
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)
def test_has_permission_filter(describe, test_client, session, admin_user, logged_in, perm): with describe('setup'), logged_in(admin_user): course = helpers.to_db_object(helpers.create_course(test_client), m.Course) rol1 = m.CourseRole('rol1', course, hidden=False) rol2 = m.CourseRole('rol2', course, hidden=False) session.add(rol1) session.add(rol2) session.commit() user1 = helpers.create_user_with_role(session, 'rol1', course) user2 = helpers.create_user_with_role(session, 'rol2', course) rol1.set_permission(perm, True) rol2.set_permission(perm, False) session.commit() with describe('Role is include if (and only if) the permission is true'): roles = m.CourseRole.get_roles_with_permission(perm).all() assert rol1 in roles assert rol2 not in roles with describe('Can be filtered further'): roles = m.CourseRole.get_roles_with_permission(perm).filter( m.CourseRole.course == course).all() assert all(r.course_id == course.id for r in roles) assert 'rol1' in [r.name for r in roles] assert 'Teacher' in [r.name for r in roles] with describe('User show up if permission is true not otherwise'): query = course.get_all_users_in_course( include_test_students=False, with_permission=perm, ) assert sorted([user1.id, admin_user.id]) == sorted(user.id for user, _ in query)
def 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
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]
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)
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
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)
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']
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()
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
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)
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)
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)
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)
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
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)
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
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, })
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.*' ) } )
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', }, )
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