class CoursesLTIAPITests(ComPAIRAPITestCase): def setUp(self): super(CoursesLTIAPITests, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() def test_delete_course(self): # test unlinking of lti contexts when course deleted course = self.data.get_course() url = '/api/courses/' + course.uuid lti_consumer = self.lti_data.get_consumer() lti_context1 = self.lti_data.create_context( lti_consumer, compair_course_id=course.id) lti_context2 = self.lti_data.create_context( lti_consumer, compair_course_id=course.id) lti_resource_link1 = self.lti_data.create_resource_link( lti_consumer, lti_context=lti_context2, compair_assignment=self.data.assignments[0]) lti_resource_link2 = self.lti_data.create_resource_link( lti_consumer, lti_context=lti_context2, compair_assignment=self.data.assignments[1]) with self.login(self.data.get_authorized_instructor().username): rv = self.client.delete(url) self.assert200(rv) self.assertEqual(course.uuid, rv.json['id']) self.assertIsNone(lti_context1.compair_course_id) self.assertIsNone(lti_context2.compair_course_id) self.assertIsNone(lti_resource_link1.compair_assignment_id) self.assertIsNone(lti_resource_link2.compair_assignment_id)
def setUp(self): super(ImpersonationAPITests, self).setUp() self.data = SimpleAssignmentTestData() self.course_base_url = '/api/courses' self.assignment_base_url = self.course_base_url + '/' + self.data.get_course( ).uuid + '/assignments' self.impersonate_base_url = '/api/impersonate' self.profile_base_url = '/api/users' self.session_base_url = '/api/session' self.assignment_main_course = self.data.get_assignments()[0] self.student_main_course = self.data.get_authorized_student() self.student_second_course = self.data.get_unauthorized_student() self.instructor_main_course = self.data.get_authorized_instructor() self.instructor_second_course = self.data.get_unauthorized_instructor( )
class CoursesLTIAPITests(ComPAIRAPITestCase): def setUp(self): super(CoursesLTIAPITests, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() def test_delete_course(self): # test unlinking of lti contexts when course deleted course = self.data.get_course() url = '/api/courses/' + course.uuid lti_consumer = self.lti_data.get_consumer() lti_context1 = self.lti_data.create_context( lti_consumer, compair_course_id=course.id ) lti_context2 = self.lti_data.create_context( lti_consumer, compair_course_id=course.id ) lti_resource_link1 = self.lti_data.create_resource_link( lti_consumer, lti_context=lti_context2, compair_assignment=self.data.assignments[0] ) lti_resource_link2 = self.lti_data.create_resource_link( lti_consumer, lti_context=lti_context2, compair_assignment=self.data.assignments[1] ) with self.login(self.data.get_authorized_instructor().username): rv = self.client.delete(url) self.assert200(rv) self.assertEqual(course.uuid, rv.json['id']) self.assertIsNone(lti_context1.compair_course_id) self.assertIsNone(lti_context2.compair_course_id) self.assertIsNone(lti_resource_link1.compair_assignment_id) self.assertIsNone(lti_resource_link2.compair_assignment_id)
def setUp(self): super(ImpersonationAPITests, self).setUp() self.data = SimpleAssignmentTestData() self.course_base_url = '/api/courses' self.assignment_base_url = self.course_base_url + '/' + self.data.get_course().uuid + '/assignments' self.impersonate_base_url = '/api/impersonate' self.profile_base_url = '/api/users' self.session_base_url = '/api/session' self.assignment_main_course = self.data.get_assignments()[0] self.student_main_course = self.data.get_authorized_student(); self.student_second_course = self.data.get_unauthorized_student(); self.instructor_main_course = self.data.get_authorized_instructor(); self.instructor_second_course = self.data.get_unauthorized_instructor();
class LoginAPITests(ComPAIRAPITestCase): def setUp(self): super(LoginAPITests, self).setUp() self.data = SimpleAssignmentTestData() def test_app_login(self): params = {'username': '******', 'password': '******'} # test app login disabled self.app.config['APP_LOGIN_ENABLED'] = False rv = self.client.post('/api/login', data=json.dumps(params), content_type='application/json', follow_redirects=True) self.assert403(rv) # test app login enabled self.app.config['APP_LOGIN_ENABLED'] = True rv = self.client.post('/api/login', data=json.dumps(params), content_type='application/json', follow_redirects=True) self.assert200(rv) def test_cas_login(self): auth_data = ThirdPartyAuthTestData() user = self.data.create_user(SystemRole.instructor) third_party_user = auth_data.create_third_party_user(user=user) response_mock = mock.MagicMock() response_mock.success = True response_mock.user = third_party_user.unique_identifier response_mock.attributes = None with mock.patch('compair.api.login.validate_cas_ticket', return_value=response_mock): # test cas login disabled self.app.config['CAS_LOGIN_ENABLED'] = False rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert403(rv) # test cas login enabled self.app.config['CAS_LOGIN_ENABLED'] = True rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv)
class LoginAPITests(ComPAIRAPITestCase): def setUp(self): super(LoginAPITests, self).setUp() self.data = SimpleAssignmentTestData() def test_app_login(self): params = { 'username': '******', 'password': '******' } # test app login disabled self.app.config['APP_LOGIN_ENABLED'] = False rv = self.client.post('/api/login', data=json.dumps(params), content_type='application/json', follow_redirects=True) self.assert403(rv) # test app login enabled self.app.config['APP_LOGIN_ENABLED'] = True rv = self.client.post('/api/login', data=json.dumps(params), content_type='application/json', follow_redirects=True) self.assert200(rv) def test_cas_login(self): auth_data = ThirdPartyAuthTestData() user = self.data.create_user(SystemRole.instructor) third_party_user = auth_data.create_third_party_user(user=user) with mock.patch('flask_cas.CAS.username', new_callable=mock.PropertyMock) as mocked_cas_username: # test cas login disabled self.app.config['CAS_LOGIN_ENABLED'] = False mocked_cas_username.return_value = third_party_user.unique_identifier rv = self.client.get('/api/auth/cas', data={}, content_type='application/json', follow_redirects=True) self.assert403(rv) # test cas login enabled self.app.config['CAS_LOGIN_ENABLED'] = True mocked_cas_username.return_value = third_party_user.unique_identifier rv = self.client.get('/api/auth/cas', data={}, content_type='application/json', follow_redirects=True) self.assert200(rv)
class ImpersonationAPITests(ComPAIRAPITestCase): def setUp(self): super(ImpersonationAPITests, self).setUp() self.data = SimpleAssignmentTestData() self.course_base_url = '/api/courses' self.assignment_base_url = self.course_base_url + '/' + self.data.get_course().uuid + '/assignments' self.impersonate_base_url = '/api/impersonate' self.profile_base_url = '/api/users' self.session_base_url = '/api/session' self.assignment_main_course = self.data.get_assignments()[0] self.student_main_course = self.data.get_authorized_student(); self.student_second_course = self.data.get_unauthorized_student(); self.instructor_main_course = self.data.get_authorized_instructor(); self.instructor_second_course = self.data.get_unauthorized_instructor(); def _start_impersonation(self, pretending): return self.client.post(self.impersonate_base_url + '/' + pretending.uuid) def _end_impersonation(self): return self.client.delete(self.impersonate_base_url) def _verify_impersonation(self, original=None, pretending=None): rv = self.client.get(self.impersonate_base_url) self.assert200(rv) self.assertEquals(rv.json.get('impersonating'), True) if original: self.assertEquals(rv.json.get('original_user').get('id'), original.uuid) if pretending: rv = self.client.get(self.session_base_url) self.assert200(rv) self.assertEqual(rv.json.get('id'), pretending.uuid) def _is_not_impersonating(self): rv = self.client.get(self.impersonate_base_url) self.assert200(rv) self.assertEquals(rv.json.get('impersonating'), False) def test_impersonation_requires_login(self): self.assert401(self._start_impersonation(self.instructor_main_course)) self.assert401(self._start_impersonation(self.student_main_course)) def test_student_cannot_impersonate(self): with self.login(self.student_main_course.username): self.assert403(self._start_impersonation(self.instructor_main_course)) self.assert403(self._start_impersonation(self.student_second_course)) def test_instructor_can_impersonate_own_students(self): with self.impersonate(self.instructor_main_course, self.student_main_course): self._verify_impersonation(original=self.instructor_main_course, pretending=self.student_main_course) def test_instructor_cannot_impersonate_student_not_in_his_course(self): with self.login(self.instructor_main_course.username): self.assert403(self._start_impersonation(self.student_second_course)) with self.login(self.instructor_second_course.username): self.assert403(self._start_impersonation(self.student_main_course)) def test_instructor_cannot_impersonate_admin(self): with self.login(self.instructor_main_course.username): self.assert403(self._start_impersonation(DefaultFixture.ROOT_USER)) def test_admin_can_impersonate_students_but_not_instructors(self): with self.login(DefaultFixture.ROOT_USER.username): self.assert200(self._start_impersonation(self.student_main_course)) self.assert403(self._start_impersonation(self.instructor_main_course)) def test_end_impersonation(self): # Impersonation will block non-GET traffic, but should allow end impersonation (a DELETE call) with self.login(self.instructor_main_course.username): self.assert200(self._start_impersonation(self.student_main_course)) self._verify_impersonation(original=self.instructor_main_course, pretending=self.student_main_course) self.assert200(self._end_impersonation()) self._is_not_impersonating() def test_cannot_logout_during_impersonation(self): with self.impersonate(self.instructor_main_course, self.student_main_course): # explicit logout should fail self.assert403(self.client.delete('/api/logout', follow_redirects=True)) # still logged in and impersonating self._verify_impersonation(original=self.instructor_main_course, pretending=self.student_main_course) def test_access_right_after_impersonation(self): comparisons_of_main_course_assignment_url = \ self.assignment_base_url + '/' + self.assignment_main_course.uuid + '/users/comparisons?page=1&perPage=5' with self.login(self.instructor_main_course.username): self.assert200(self._start_impersonation(self.student_main_course)) # no access when impersonating as student self.assert403(self.client.get(comparisons_of_main_course_assignment_url)) self._end_impersonation() # resume access after impersonation self.assert200(self.client.get(comparisons_of_main_course_assignment_url))
class LoginAPITests(ComPAIRAPITestCase): def setUp(self): super(LoginAPITests, self).setUp() self.data = SimpleAssignmentTestData() def test_app_login(self): params = { 'username': '******', 'password': '******' } # test app login disabled self.app.config['APP_LOGIN_ENABLED'] = False rv = self.client.post('/api/login', data=json.dumps(params), content_type='application/json', follow_redirects=True) self.assert403(rv) # test app login enabled self.app.config['APP_LOGIN_ENABLED'] = True rv = self.client.post('/api/login', data=json.dumps(params), content_type='application/json', follow_redirects=True) self.assert200(rv) def test_logout(self): with self.login('root'): url = '/api/logout' rv = self.client.delete(url) self.assert200(rv) # can't logout during impersonation with self.impersonate(DefaultFixture.ROOT_USER, self.data.get_authorized_student()): url = '/api/logout' rv = self.client.delete(url) self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) def test_cas_login(self): auth_data = ThirdPartyAuthTestData() # test login new user for system_role in [SystemRole.student, SystemRole.instructor, SystemRole.sys_admin]: unique_identifier = system_role.value + "_no_attributes" response_mock = mock.MagicMock() response_mock.success = True response_mock.user = unique_identifier response_mock.attributes = None self.app.config['CAS_ATTRIBUTE_FIRST_NAME'] = None self.app.config['CAS_ATTRIBUTE_LAST_NAME'] = None self.app.config['CAS_ATTRIBUTE_STUDENT_NUMBER'] = None self.app.config['CAS_ATTRIBUTE_EMAIL'] = None self.app.config['CAS_ATTRIBUTE_USER_ROLE'] = None self.app.config['CAS_INSTRUCTOR_ROLE_VALUES'] = {} self.app.config['CAS_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = None with mock.patch('compair.api.login.validate_cas_ticket', return_value=response_mock): # test cas login disabled self.app.config['CAS_LOGIN_ENABLED'] = False rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert403(rv) # test cas login enabled with unsuccessful login self.app.config['CAS_LOGIN_ENABLED'] = True response_mock.success = False rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) # check session with self.client.session_transaction() as sess: # check that user is logged in self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_TYPE'), 'CAS') self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_MSG'), 'Login Failed. CAS ticket was invalid.') # test cas login enabled with unsuccessful login response_mock.success = True response_mock.user = None rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) # check session with self.client.session_transaction() as sess: # check that user is logged in self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_TYPE'), 'CAS') self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_MSG'), 'Login Failed. Expecting CAS username to be set.') # test cas login enabled with successful login response_mock.user = unique_identifier rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.cas)\ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.system_role, SystemRole.student) self.assertIsNone(third_party_user.user.firstname) self.assertIsNone(third_party_user.user.lastname) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") self.assertIsNone(third_party_user.user.email) self.assertIsNone(third_party_user.user.student_number) self.assertIsNone(third_party_user.user.global_unique_identifier) with self.client.session_transaction() as sess: self.assertEqual(sess.get('user_id'), str(third_party_user.user.id)) # unused attributes unique_identifier = system_role.value + "_with_unused_attributes" response_mock.user = unique_identifier response_mock.attributes = { 'firstName': 'f_name', 'lastName': 'l_name', 'studentNumber': "student1" if system_role == SystemRole.student else None, 'email': '*****@*****.**', 'system_role_field': system_role.value, 'puid': system_role.value+"_puid", } rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.cas)\ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.system_role, SystemRole.student) self.assertIsNone(third_party_user.user.firstname) self.assertIsNone(third_party_user.user.lastname) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") self.assertIsNone(third_party_user.user.email) self.assertIsNone(third_party_user.user.student_number) self.assertIsNone(third_party_user.user.global_unique_identifier) # used attributes and no valid instructor values self.app.config['CAS_ATTRIBUTE_FIRST_NAME'] = 'firstName' self.app.config['CAS_ATTRIBUTE_LAST_NAME'] = 'lastName' self.app.config['CAS_ATTRIBUTE_STUDENT_NUMBER'] = 'studentNumber' self.app.config['CAS_ATTRIBUTE_EMAIL'] = 'email' self.app.config['CAS_ATTRIBUTE_USER_ROLE'] = 'system_role_field' self.app.config['CAS_INSTRUCTOR_ROLE_VALUES'] = {} self.app.config['CAS_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = 'puid' unique_identifier = system_role.value + "_with_used_attributes" response_mock.user = unique_identifier rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.cas) \ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.system_role, SystemRole.student) self.assertEqual(third_party_user.user.firstname, 'f_name') self.assertEqual(third_party_user.user.lastname, 'l_name') self.assertEqual(third_party_user.user.email, '*****@*****.**') self.assertEqual(third_party_user.user.global_unique_identifier, system_role.value+"_puid") if system_role == SystemRole.student: self.assertEqual(third_party_user.user.student_number, 'student1') else: self.assertIsNone(third_party_user.user.student_number) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") # used attributes and valid instructor values unique_identifier = system_role.value + "_with_used_attributes2" response_mock.user = unique_identifier if system_role == SystemRole.student: response_mock.attributes['studentNumber'] = "student2" response_mock.attributes['puid'] = system_role.value+"_puid2" self.app.config['CAS_INSTRUCTOR_ROLE_VALUES'] = {SystemRole.sys_admin.value, SystemRole.instructor.value} rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.cas)\ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.firstname, 'f_name') self.assertEqual(third_party_user.user.lastname, 'l_name') self.assertEqual(third_party_user.user.email, '*****@*****.**') self.assertEqual(third_party_user.user.global_unique_identifier, system_role.value+"_puid2") if system_role == SystemRole.student: self.assertEqual(third_party_user.user.system_role, SystemRole.student) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") self.assertEqual(third_party_user.user.student_number, 'student2') else: self.assertEqual(third_party_user.user.system_role, SystemRole.instructor) self.assertEqual(third_party_user.user.displayname, "f_name l_name") self.assertIsNone(third_party_user.user.student_number) # test login existing user for system_role in [SystemRole.student, SystemRole.instructor, SystemRole.sys_admin]: self.app.config['CAS_ATTRIBUTE_FIRST_NAME'] = None self.app.config['CAS_ATTRIBUTE_LAST_NAME'] = None self.app.config['CAS_ATTRIBUTE_STUDENT_NUMBER'] = None self.app.config['CAS_ATTRIBUTE_EMAIL'] = None self.app.config['CAS_ATTRIBUTE_USER_ROLE'] = None self.app.config['CAS_INSTRUCTOR_ROLE_VALUES'] = {} self.app.config['CAS_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = None user = self.data.create_user(system_role) third_party_user = auth_data.create_third_party_user(user=user, third_party_type=ThirdPartyType.cas) original_firstname = user.firstname original_lastname = user.lastname original_email = user.email original_student_number = user.student_number new_student_number = original_student_number+"123" if user.student_number else None response_mock = mock.MagicMock() response_mock.success = True response_mock.user = third_party_user.unique_identifier response_mock.attributes = None with mock.patch('compair.api.login.validate_cas_ticket', return_value=response_mock): # test cas login disabled self.app.config['CAS_LOGIN_ENABLED'] = False rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert403(rv) # test cas login enabled self.app.config['CAS_LOGIN_ENABLED'] = True rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) # test overwrite disabled with no attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = True self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = True rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) # test overwrite enabled with no attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = False self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = False rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) # test overwrite enabled with no attributes self.app.config['CAS_ATTRIBUTE_FIRST_NAME'] = 'firstName' self.app.config['CAS_ATTRIBUTE_LAST_NAME'] = 'lastName' self.app.config['CAS_ATTRIBUTE_STUDENT_NUMBER'] = 'studentNumber' self.app.config['CAS_ATTRIBUTE_EMAIL'] = 'email' self.app.config['CAS_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = 'puid' rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) response_mock.attributes = { 'firstName': 'f_name', 'lastName': 'l_name', 'studentNumber': new_student_number, 'email': '*****@*****.**', 'puid': 'should_be_ignored_since_already_linked' } # test overwrite disabled with attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = True self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = True rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) # test overwrite enabled with attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = False self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = False rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) if system_role == SystemRole.student: self.assertEqual(user.firstname, 'f_name') self.assertEqual(user.lastname, 'l_name') self.assertEqual(user.email, '*****@*****.**') self.assertEqual(user.student_number, new_student_number) else: self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) self.app.config['CAS_ATTRIBUTE_USER_ROLE'] = 'system_role_field' self.app.config['CAS_INSTRUCTOR_ROLE_VALUES'] = {SystemRole.instructor.value} # test automatic upgrading of system role for existing accounts for third_party_system_role in [SystemRole.student, SystemRole.instructor, SystemRole.sys_admin]: user = self.data.create_user(system_role) third_party_user = auth_data.create_third_party_user(user=user, third_party_type=ThirdPartyType.cas) db.session.commit() response_mock.user = third_party_user.unique_identifier response_mock.attributes = { 'system_role_field': third_party_system_role.value } with mock.patch('compair.api.login.validate_cas_ticket', return_value=response_mock): rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) # compair user system role will upgrade if system_role == SystemRole.student: # cannot upgrade to admin if third_party_system_role == SystemRole.instructor: self.assertEqual(user.system_role, SystemRole.instructor) else: self.assertEqual(user.system_role, SystemRole.student) elif system_role == SystemRole.instructor: # cannot upgrade to admin and shouldn't downgrade to student self.assertEqual(user.system_role, SystemRole.instructor) elif system_role == SystemRole.sys_admin: # shouldn't downgrade self.assertEqual(user.system_role, SystemRole.sys_admin) def test_saml_login(self): auth_data = ThirdPartyAuthTestData() # test login new user for system_role in [SystemRole.student, SystemRole.instructor, SystemRole.sys_admin]: unique_identifier = system_role.value + "_no_attributes" response_mock = mock.MagicMock() response_mock.__errors = [] response_mock.is_authenticated.return_value = True response_mock.get_attributes.return_value = { 'urn:oid:0.9.2342.19200300.100.1.1': [unique_identifier] } response_mock.get_nameid.return_value = "saml_mock_nameid" response_mock.get_session_index.return_value = "saml_session_index" self.app.config['SAML_ATTRIBUTE_FIRST_NAME'] = None self.app.config['SAML_ATTRIBUTE_LAST_NAME'] = None self.app.config['SAML_ATTRIBUTE_STUDENT_NUMBER'] = None self.app.config['SAML_ATTRIBUTE_EMAIL'] = None self.app.config['SAML_ATTRIBUTE_USER_ROLE'] = None self.app.config['SAML_INSTRUCTOR_ROLE_VALUES'] = {} self.app.config['SAML_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = None with mock.patch('compair.api.login.get_saml_auth_response', return_value=response_mock): # test saml login disabled self.app.config['SAML_LOGIN_ENABLED'] = False rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert403(rv) # test saml login enabled with unsuccessful login self.app.config['SAML_LOGIN_ENABLED'] = True response_mock.is_authenticated.return_value = False rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) # check session with self.client.session_transaction() as sess: # check that user is logged in self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_TYPE'), 'SAML') self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_MSG'), 'Login Failed.') # test saml login enabled with unsuccessful login response_mock.is_authenticated.return_value = True response_mock.__errors = ['error1', 'error2'] rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) # check session with self.client.session_transaction() as sess: # check that user is logged in self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_TYPE'), 'SAML') self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_MSG'), 'Login Failed.') # test saml login enabled with successful login response_mock.__errors = [] rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.saml)\ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.system_role, SystemRole.student) self.assertIsNone(third_party_user.user.firstname) self.assertIsNone(third_party_user.user.lastname) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") self.assertIsNone(third_party_user.user.email) self.assertIsNone(third_party_user.user.student_number) self.assertIsNone(third_party_user.user.global_unique_identifier) with self.client.session_transaction() as sess: self.assertEqual(sess.get('user_id'), str(third_party_user.user.id)) # unused attributes unique_identifier = system_role.value + "_with_unused_attributes" response_mock.get_attributes.return_value = { 'urn:oid:1.3.6.1.4.1.5923.1.1.1.9': ['*****@*****.**', '*****@*****.**'], 'urn:oid:2.5.4.42': ['f_name'], 'urn:oid:0.9.2342.19200300.100.1.1': [unique_identifier], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.1': ['Member'], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10': [None], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7': [system_role.value], 'urn:oid:2.5.4.4': ['l_name'], 'urn:oid:2.5.4.3': ['Me Myself And I'], 'urn:oid:2.5.4.20': ['555-5555'], 'urn:oid:2.16.840.1.113730.3.1.3': ["student1"] if system_role == SystemRole.student else [], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6': ['*****@*****.**'] } rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.saml)\ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.system_role, SystemRole.student) self.assertIsNone(third_party_user.user.firstname) self.assertIsNone(third_party_user.user.lastname) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") self.assertIsNone(third_party_user.user.email) self.assertIsNone(third_party_user.user.student_number) self.assertIsNone(third_party_user.user.global_unique_identifier) # used attributes and no valid instructor values self.app.config['SAML_ATTRIBUTE_FIRST_NAME'] = 'urn:oid:2.5.4.42' self.app.config['SAML_ATTRIBUTE_LAST_NAME'] = 'urn:oid:2.5.4.4' self.app.config['SAML_ATTRIBUTE_STUDENT_NUMBER'] = 'urn:oid:2.16.840.1.113730.3.1.3' self.app.config['SAML_ATTRIBUTE_EMAIL'] = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6' self.app.config['SAML_ATTRIBUTE_USER_ROLE'] = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7' self.app.config['SAML_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = 'puid' self.app.config['SAML_INSTRUCTOR_ROLE_VALUES'] = {} unique_identifier = system_role.value + "_with_used_attributes" response_mock.get_attributes.return_value = { 'urn:oid:1.3.6.1.4.1.5923.1.1.1.9': ['*****@*****.**', '*****@*****.**'], 'urn:oid:2.5.4.42': ['f_name'], 'urn:oid:0.9.2342.19200300.100.1.1': [unique_identifier], 'puid': [system_role.value+"_puid"], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.1': ['Member'], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10': [None], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7': [system_role.value], 'urn:oid:2.5.4.4': ['l_name'], 'urn:oid:2.5.4.3': ['Me Myself And I'], 'urn:oid:2.5.4.20': ['555-5555'], 'urn:oid:2.16.840.1.113730.3.1.3': ["student1"] if system_role == SystemRole.student else [], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6': ['*****@*****.**'], } rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.saml) \ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.system_role, SystemRole.student) self.assertEqual(third_party_user.user.firstname, 'f_name') self.assertEqual(third_party_user.user.lastname, 'l_name') self.assertEqual(third_party_user.user.email, '*****@*****.**') self.assertEqual(third_party_user.user.global_unique_identifier, system_role.value+"_puid") if system_role == SystemRole.student: self.assertEqual(third_party_user.user.student_number, 'student1') else: self.assertIsNone(third_party_user.user.student_number) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") # used attributes and valid instructor values unique_identifier = system_role.value + "_with_used_attributes2" response_mock.get_attributes.return_value = { 'urn:oid:1.3.6.1.4.1.5923.1.1.1.9': ['*****@*****.**', '*****@*****.**'], 'urn:oid:2.5.4.42': ['f_name'], 'urn:oid:0.9.2342.19200300.100.1.1': [unique_identifier], 'puid': [system_role.value+"_puid2"], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.1': ['Member'], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10': [None], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7': [system_role.value], 'urn:oid:2.5.4.4': ['l_name'], 'urn:oid:2.5.4.3': ['Me Myself And I'], 'urn:oid:2.5.4.20': ['555-5555'], 'urn:oid:2.16.840.1.113730.3.1.3': ["student2"] if system_role == SystemRole.student else [], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6': ['*****@*****.**'] } self.app.config['SAML_INSTRUCTOR_ROLE_VALUES'] = {SystemRole.sys_admin.value, SystemRole.instructor.value} rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.saml)\ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.firstname, 'f_name') self.assertEqual(third_party_user.user.lastname, 'l_name') self.assertEqual(third_party_user.user.email, '*****@*****.**') self.assertEqual(third_party_user.user.global_unique_identifier, system_role.value+"_puid2") if system_role == SystemRole.student: self.assertEqual(third_party_user.user.system_role, SystemRole.student) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") self.assertEqual(third_party_user.user.student_number, 'student2') else: self.assertEqual(third_party_user.user.system_role, SystemRole.instructor) self.assertEqual(third_party_user.user.displayname, "f_name l_name") self.assertIsNone(third_party_user.user.student_number) # test login existing user for system_role in [SystemRole.student, SystemRole.instructor, SystemRole.sys_admin]: self.app.config['SAML_ATTRIBUTE_FIRST_NAME'] = None self.app.config['SAML_ATTRIBUTE_LAST_NAME'] = None self.app.config['SAML_ATTRIBUTE_STUDENT_NUMBER'] = None self.app.config['SAML_ATTRIBUTE_EMAIL'] = None self.app.config['SAML_ATTRIBUTE_USER_ROLE'] = None self.app.config['SAML_INSTRUCTOR_ROLE_VALUES'] = {} self.app.config['SAML_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = None user = self.data.create_user(system_role) third_party_user = auth_data.create_third_party_user(user=user, third_party_type=ThirdPartyType.saml) original_firstname = user.firstname original_lastname = user.lastname original_email = user.email original_student_number = user.student_number new_student_number = original_student_number+"123" if user.student_number else None response_mock = mock.MagicMock() response_mock.__errors = [] response_mock.is_authenticated.return_value = True response_mock.get_attributes.return_value = { 'urn:oid:0.9.2342.19200300.100.1.1': [third_party_user.unique_identifier] } response_mock.get_nameid.return_value = "saml_mock_nameid" response_mock.get_session_index.return_value = "saml_session_index" with mock.patch('compair.api.login.get_saml_auth_response', return_value=response_mock): # test saml login disabled self.app.config['SAML_LOGIN_ENABLED'] = False rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert403(rv) # test saml login enabled self.app.config['SAML_LOGIN_ENABLED'] = True rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) # test overwrite disabled with no attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = True self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = True rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) # test overwrite enabled with no attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = False self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = False rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) # test overwrite enabled with no attributes self.app.config['SAML_ATTRIBUTE_FIRST_NAME'] = 'urn:oid:2.5.4.42' self.app.config['SAML_ATTRIBUTE_LAST_NAME'] = 'urn:oid:2.5.4.4' self.app.config['SAML_ATTRIBUTE_STUDENT_NUMBER'] = 'urn:oid:2.16.840.1.113730.3.1.3' self.app.config['SAML_ATTRIBUTE_EMAIL'] = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6' self.app.config['SAML_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = 'puid' rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) response_mock.get_attributes.return_value = { 'urn:oid:1.3.6.1.4.1.5923.1.1.1.9': ['*****@*****.**', '*****@*****.**'], 'urn:oid:2.5.4.42': ['f_name'], 'urn:oid:0.9.2342.19200300.100.1.1': [third_party_user.unique_identifier], 'puid': ["should_be_ignored_since_already_linked"], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.1': ['Member', 'Staff'], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10': [None], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7': ['urn:mace:dir:entitlement:common-lib-terms'], 'urn:oid:2.5.4.4': ['l_name'], 'urn:oid:2.5.4.3': ['Me Myself And I'], 'urn:oid:2.5.4.20': ['555-5555'], 'urn:oid:2.16.840.1.113730.3.1.3': [new_student_number], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6': ['*****@*****.**'] } # test overwrite disabled with attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = True self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = True rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) # test overwrite enabled with attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = False self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = False rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) if system_role == SystemRole.student: self.assertEqual(user.firstname, 'f_name') self.assertEqual(user.lastname, 'l_name') self.assertEqual(user.email, '*****@*****.**') self.assertEqual(user.student_number, new_student_number) else: self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) self.app.config['SAML_ATTRIBUTE_USER_ROLE'] = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7' self.app.config['SAML_INSTRUCTOR_ROLE_VALUES'] = {SystemRole.instructor.value} # test automatic upgrading of system role for existing accounts for third_party_system_role in [SystemRole.student, SystemRole.instructor, SystemRole.sys_admin]: user = self.data.create_user(system_role) third_party_user = auth_data.create_third_party_user(user=user, third_party_type=ThirdPartyType.saml) db.session.commit() response_mock.get_attributes.return_value = { 'urn:oid:0.9.2342.19200300.100.1.1': [third_party_user.unique_identifier], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7': [third_party_system_role.value] } with mock.patch('compair.api.login.get_saml_auth_response', return_value=response_mock): rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) # compair user system role will upgrade if system_role == SystemRole.student: # cannot upgrade to admin if third_party_system_role == SystemRole.instructor: self.assertEqual(user.system_role, SystemRole.instructor) else: self.assertEqual(user.system_role, SystemRole.student) elif system_role == SystemRole.instructor: # cannot upgrade to admin and shouldn't downgrade to student self.assertEqual(user.system_role, SystemRole.instructor) elif system_role == SystemRole.sys_admin: # shouldn't downgrade self.assertEqual(user.system_role, SystemRole.sys_admin) def test_saml_metadata(self): # test saml login enabled self.app.config['SAML_LOGIN_ENABLED'] = True self.app.config['SAML_EXPOSE_METADATA_ENDPOINT'] = True rv = self.client.get('/api/saml/metadata', headers={'Accept': 'text/xml'}) self.assert200(rv) # test saml login disabled self.app.config['SAML_LOGIN_ENABLED'] = False self.app.config['SAML_EXPOSE_METADATA_ENDPOINT'] = True rv = self.client.get('/api/saml/metadata', headers={'Accept': 'text/xml'}) self.assert404(rv) # test saml metadata disabled self.app.config['SAML_LOGIN_ENABLED'] = True self.app.config['SAML_EXPOSE_METADATA_ENDPOINT'] = False rv = self.client.get('/api/saml/metadata', headers={'Accept': 'text/xml'}) self.assert404(rv)
def setUp(self): super(LoginAPITests, self).setUp() self.data = SimpleAssignmentTestData()
class ImpersonationAPITests(ComPAIRAPITestCase): def setUp(self): super(ImpersonationAPITests, self).setUp() self.data = SimpleAssignmentTestData() self.course_base_url = '/api/courses' self.assignment_base_url = self.course_base_url + '/' + self.data.get_course( ).uuid + '/assignments' self.impersonate_base_url = '/api/impersonate' self.profile_base_url = '/api/users' self.session_base_url = '/api/session' self.assignment_main_course = self.data.get_assignments()[0] self.student_main_course = self.data.get_authorized_student() self.student_second_course = self.data.get_unauthorized_student() self.instructor_main_course = self.data.get_authorized_instructor() self.instructor_second_course = self.data.get_unauthorized_instructor( ) def _start_impersonation(self, pretending): return self.client.post(self.impersonate_base_url + '/' + pretending.uuid) def _end_impersonation(self): return self.client.delete(self.impersonate_base_url) def _verify_impersonation(self, original=None, pretending=None): rv = self.client.get(self.impersonate_base_url) self.assert200(rv) self.assertEquals(rv.json.get('impersonating'), True) if original: self.assertEquals( rv.json.get('original_user').get('id'), original.uuid) if pretending: rv = self.client.get(self.session_base_url) self.assert200(rv) self.assertEqual(rv.json.get('id'), pretending.uuid) def _is_not_impersonating(self): rv = self.client.get(self.impersonate_base_url) self.assert200(rv) self.assertEquals(rv.json.get('impersonating'), False) def test_impersonation_requires_login(self): self.assert401(self._start_impersonation(self.instructor_main_course)) self.assert401(self._start_impersonation(self.student_main_course)) def test_student_cannot_impersonate(self): with self.login(self.student_main_course.username): self.assert403( self._start_impersonation(self.instructor_main_course)) self.assert403( self._start_impersonation(self.student_second_course)) def test_instructor_can_impersonate_own_students(self): with self.impersonate(self.instructor_main_course, self.student_main_course): self._verify_impersonation(original=self.instructor_main_course, pretending=self.student_main_course) def test_instructor_cannot_impersonate_student_not_in_his_course(self): with self.login(self.instructor_main_course.username): self.assert403( self._start_impersonation(self.student_second_course)) with self.login(self.instructor_second_course.username): self.assert403(self._start_impersonation(self.student_main_course)) def test_instructor_cannot_impersonate_admin(self): with self.login(self.instructor_main_course.username): self.assert403(self._start_impersonation(DefaultFixture.ROOT_USER)) def test_admin_can_impersonate_students_but_not_instructors(self): with self.login(DefaultFixture.ROOT_USER.username): self.assert200(self._start_impersonation(self.student_main_course)) self.assert403( self._start_impersonation(self.instructor_main_course)) def test_end_impersonation(self): # Impersonation will block non-GET traffic, but should allow end impersonation (a DELETE call) with self.login(self.instructor_main_course.username): self.assert200(self._start_impersonation(self.student_main_course)) self._verify_impersonation(original=self.instructor_main_course, pretending=self.student_main_course) self.assert200(self._end_impersonation()) self._is_not_impersonating() def test_cannot_logout_during_impersonation(self): with self.impersonate(self.instructor_main_course, self.student_main_course): # explicit logout should fail self.assert403( self.client.delete('/api/logout', follow_redirects=True)) # still logged in and impersonating self._verify_impersonation(original=self.instructor_main_course, pretending=self.student_main_course) def test_access_right_after_impersonation(self): comparisons_of_main_course_assignment_url = \ self.assignment_base_url + '/' + self.assignment_main_course.uuid + '/users/comparisons?page=1&perPage=5' with self.login(self.instructor_main_course.username): self.assert200(self._start_impersonation(self.student_main_course)) # no access when impersonating as student self.assert403( self.client.get(comparisons_of_main_course_assignment_url)) self._end_impersonation() # resume access after impersonation self.assert200( self.client.get(comparisons_of_main_course_assignment_url))
def setUp(self): super(ComPAIRXAPITestCase, self).setUp() self.data = SimpleAssignmentTestData() self.user = self.data.authorized_student self.assignment = self.data.assignments[0]
class LoginAPITests(ComPAIRAPITestCase): def setUp(self): super(LoginAPITests, self).setUp() self.data = SimpleAssignmentTestData() def test_app_login(self): params = {'username': '******', 'password': '******'} # test app login disabled self.app.config['APP_LOGIN_ENABLED'] = False rv = self.client.post('/api/login', data=json.dumps(params), content_type='application/json', follow_redirects=True) self.assert403(rv) # test app login enabled self.app.config['APP_LOGIN_ENABLED'] = True rv = self.client.post('/api/login', data=json.dumps(params), content_type='application/json', follow_redirects=True) self.assert200(rv) def test_logout(self): with self.login('root'): url = '/api/logout' rv = self.client.delete(url) self.assert200(rv) # can't logout during impersonation with self.impersonate(DefaultFixture.ROOT_USER, self.data.get_authorized_student()): url = '/api/logout' rv = self.client.delete(url) self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) def test_cas_login(self): auth_data = ThirdPartyAuthTestData() # test login new user for system_role in [ SystemRole.student, SystemRole.instructor, SystemRole.sys_admin ]: unique_identifier = system_role.value + "_no_attributes" response_mock = mock.MagicMock() response_mock.success = True response_mock.user = unique_identifier response_mock.attributes = None self.app.config['CAS_ATTRIBUTE_FIRST_NAME'] = None self.app.config['CAS_ATTRIBUTE_LAST_NAME'] = None self.app.config['CAS_ATTRIBUTE_STUDENT_NUMBER'] = None self.app.config['CAS_ATTRIBUTE_EMAIL'] = None self.app.config['CAS_ATTRIBUTE_USER_ROLE'] = None self.app.config['CAS_INSTRUCTOR_ROLE_VALUES'] = {} self.app.config['CAS_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = None with mock.patch('compair.api.login.validate_cas_ticket', return_value=response_mock): # test cas login disabled self.app.config['CAS_LOGIN_ENABLED'] = False rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert403(rv) # test cas login enabled with unsuccessful login self.app.config['CAS_LOGIN_ENABLED'] = True response_mock.success = False rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) # check session with self.client.session_transaction() as sess: # check that user is logged in self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_TYPE'), 'CAS') self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_MSG'), 'Login Failed. CAS ticket was invalid.') # test cas login enabled with unsuccessful login response_mock.success = True response_mock.user = None rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) # check session with self.client.session_transaction() as sess: # check that user is logged in self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_TYPE'), 'CAS') self.assertEqual( sess.get('THIRD_PARTY_AUTH_ERROR_MSG'), 'Login Failed. Expecting CAS username to be set.') # test cas login enabled with successful login response_mock.user = unique_identifier rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.cas)\ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.system_role, SystemRole.student) self.assertIsNone(third_party_user.user.firstname) self.assertIsNone(third_party_user.user.lastname) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") self.assertIsNone(third_party_user.user.email) self.assertIsNone(third_party_user.user.student_number) self.assertIsNone( third_party_user.user.global_unique_identifier) with self.client.session_transaction() as sess: self.assertEqual(sess.get('_user_id'), str(third_party_user.user.id)) # unused attributes unique_identifier = system_role.value + "_with_unused_attributes" response_mock.user = unique_identifier response_mock.attributes = { 'firstName': 'f_name', 'lastName': 'l_name', 'studentNumber': "student1" if system_role == SystemRole.student else None, 'email': '*****@*****.**', 'system_role_field': system_role.value, 'puid': system_role.value + "_puid", } rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.cas)\ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.system_role, SystemRole.student) self.assertIsNone(third_party_user.user.firstname) self.assertIsNone(third_party_user.user.lastname) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") self.assertIsNone(third_party_user.user.email) self.assertIsNone(third_party_user.user.student_number) self.assertIsNone( third_party_user.user.global_unique_identifier) # used attributes and no valid instructor values self.app.config['CAS_ATTRIBUTE_FIRST_NAME'] = 'firstName' self.app.config['CAS_ATTRIBUTE_LAST_NAME'] = 'lastName' self.app.config[ 'CAS_ATTRIBUTE_STUDENT_NUMBER'] = 'studentNumber' self.app.config['CAS_ATTRIBUTE_EMAIL'] = 'email' self.app.config[ 'CAS_ATTRIBUTE_USER_ROLE'] = 'system_role_field' self.app.config['CAS_INSTRUCTOR_ROLE_VALUES'] = {} self.app.config['CAS_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = 'puid' unique_identifier = system_role.value + "_with_used_attributes" response_mock.user = unique_identifier rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.cas) \ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.system_role, SystemRole.student) self.assertEqual(third_party_user.user.firstname, 'f_name') self.assertEqual(third_party_user.user.lastname, 'l_name') self.assertEqual(third_party_user.user.email, '*****@*****.**') self.assertEqual( third_party_user.user.global_unique_identifier, system_role.value + "_puid") if system_role == SystemRole.student: self.assertEqual(third_party_user.user.student_number, 'student1') else: self.assertIsNone(third_party_user.user.student_number) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") # used attributes and valid instructor values unique_identifier = system_role.value + "_with_used_attributes2" response_mock.user = unique_identifier if system_role == SystemRole.student: response_mock.attributes['studentNumber'] = "student2" response_mock.attributes['puid'] = system_role.value + "_puid2" self.app.config['CAS_INSTRUCTOR_ROLE_VALUES'] = { SystemRole.sys_admin.value, SystemRole.instructor.value } rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.cas)\ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.firstname, 'f_name') self.assertEqual(third_party_user.user.lastname, 'l_name') self.assertEqual(third_party_user.user.email, '*****@*****.**') self.assertEqual( third_party_user.user.global_unique_identifier, system_role.value + "_puid2") if system_role == SystemRole.student: self.assertEqual(third_party_user.user.system_role, SystemRole.student) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") self.assertEqual(third_party_user.user.student_number, 'student2') else: self.assertEqual(third_party_user.user.system_role, SystemRole.instructor) self.assertEqual(third_party_user.user.displayname, "f_name l_name") self.assertIsNone(third_party_user.user.student_number) # test login existing user for system_role in [ SystemRole.student, SystemRole.instructor, SystemRole.sys_admin ]: self.app.config['CAS_ATTRIBUTE_FIRST_NAME'] = None self.app.config['CAS_ATTRIBUTE_LAST_NAME'] = None self.app.config['CAS_ATTRIBUTE_STUDENT_NUMBER'] = None self.app.config['CAS_ATTRIBUTE_EMAIL'] = None self.app.config['CAS_ATTRIBUTE_USER_ROLE'] = None self.app.config['CAS_INSTRUCTOR_ROLE_VALUES'] = {} self.app.config['CAS_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = None user = self.data.create_user(system_role) third_party_user = auth_data.create_third_party_user( user=user, third_party_type=ThirdPartyType.cas) original_firstname = user.firstname original_lastname = user.lastname original_email = user.email original_student_number = user.student_number new_student_number = original_student_number + "123" if user.student_number else None response_mock = mock.MagicMock() response_mock.success = True response_mock.user = third_party_user.unique_identifier response_mock.attributes = None with mock.patch('compair.api.login.validate_cas_ticket', return_value=response_mock): # test cas login disabled self.app.config['CAS_LOGIN_ENABLED'] = False rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert403(rv) # test cas login enabled self.app.config['CAS_LOGIN_ENABLED'] = True rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) # test overwrite disabled with no attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = True self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = True rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) # test overwrite enabled with no attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = False self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = False rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) # test overwrite enabled with no attributes self.app.config['CAS_ATTRIBUTE_FIRST_NAME'] = 'firstName' self.app.config['CAS_ATTRIBUTE_LAST_NAME'] = 'lastName' self.app.config[ 'CAS_ATTRIBUTE_STUDENT_NUMBER'] = 'studentNumber' self.app.config['CAS_ATTRIBUTE_EMAIL'] = 'email' self.app.config['CAS_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = 'puid' rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) response_mock.attributes = { 'firstName': 'f_name', 'lastName': 'l_name', 'studentNumber': new_student_number, 'email': '*****@*****.**', 'puid': 'should_be_ignored_since_already_linked' } # test overwrite disabled with attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = True self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = True rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) # test overwrite enabled with attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = False self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = False rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) if system_role == SystemRole.student: self.assertEqual(user.firstname, 'f_name') self.assertEqual(user.lastname, 'l_name') self.assertEqual(user.email, '*****@*****.**') self.assertEqual(user.student_number, new_student_number) else: self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) self.app.config['CAS_ATTRIBUTE_USER_ROLE'] = 'system_role_field' self.app.config['CAS_INSTRUCTOR_ROLE_VALUES'] = { SystemRole.instructor.value } # test automatic upgrading of system role for existing accounts for third_party_system_role in [ SystemRole.student, SystemRole.instructor, SystemRole.sys_admin ]: user = self.data.create_user(system_role) third_party_user = auth_data.create_third_party_user( user=user, third_party_type=ThirdPartyType.cas) db.session.commit() response_mock.user = third_party_user.unique_identifier response_mock.attributes = { 'system_role_field': third_party_system_role.value } with mock.patch('compair.api.login.validate_cas_ticket', return_value=response_mock): rv = self.client.get('/api/cas/auth?ticket=mock_ticket', follow_redirects=True) self.assert200(rv) # compair user system role will upgrade if system_role == SystemRole.student: # cannot upgrade to admin if third_party_system_role == SystemRole.instructor: self.assertEqual(user.system_role, SystemRole.instructor) else: self.assertEqual(user.system_role, SystemRole.student) elif system_role == SystemRole.instructor: # cannot upgrade to admin and shouldn't downgrade to student self.assertEqual(user.system_role, SystemRole.instructor) elif system_role == SystemRole.sys_admin: # shouldn't downgrade self.assertEqual(user.system_role, SystemRole.sys_admin) def test_saml_login(self): auth_data = ThirdPartyAuthTestData() # test login new user for system_role in [ SystemRole.student, SystemRole.instructor, SystemRole.sys_admin ]: unique_identifier = system_role.value + "_no_attributes" response_mock = mock.MagicMock() response_mock.__errors = [] response_mock.is_authenticated.return_value = True response_mock.get_attributes.return_value = { 'urn:oid:0.9.2342.19200300.100.1.1': [unique_identifier] } response_mock.get_nameid.return_value = "saml_mock_nameid" response_mock.get_session_index.return_value = "saml_session_index" self.app.config['SAML_ATTRIBUTE_FIRST_NAME'] = None self.app.config['SAML_ATTRIBUTE_LAST_NAME'] = None self.app.config['SAML_ATTRIBUTE_STUDENT_NUMBER'] = None self.app.config['SAML_ATTRIBUTE_EMAIL'] = None self.app.config['SAML_ATTRIBUTE_USER_ROLE'] = None self.app.config['SAML_INSTRUCTOR_ROLE_VALUES'] = {} self.app.config['SAML_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = None with mock.patch('compair.api.login.get_saml_auth_response', return_value=response_mock): # test saml login disabled self.app.config['SAML_LOGIN_ENABLED'] = False rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert403(rv) # test saml login enabled with unsuccessful login self.app.config['SAML_LOGIN_ENABLED'] = True response_mock.is_authenticated.return_value = False rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) # check session with self.client.session_transaction() as sess: # check that user is logged in self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_TYPE'), 'SAML') self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_MSG'), 'Login Failed.') # test saml login enabled with unsuccessful login response_mock.is_authenticated.return_value = True response_mock.__errors = ['error1', 'error2'] rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) # check session with self.client.session_transaction() as sess: # check that user is logged in self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_TYPE'), 'SAML') self.assertEqual(sess.get('THIRD_PARTY_AUTH_ERROR_MSG'), 'Login Failed.') # test saml login enabled with successful login response_mock.__errors = [] rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.saml)\ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.system_role, SystemRole.student) self.assertIsNone(third_party_user.user.firstname) self.assertIsNone(third_party_user.user.lastname) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") self.assertIsNone(third_party_user.user.email) self.assertIsNone(third_party_user.user.student_number) self.assertIsNone( third_party_user.user.global_unique_identifier) with self.client.session_transaction() as sess: self.assertEqual(sess.get('_user_id'), str(third_party_user.user.id)) # unused attributes unique_identifier = system_role.value + "_with_unused_attributes" response_mock.get_attributes.return_value = { 'urn:oid:1.3.6.1.4.1.5923.1.1.1.9': ['*****@*****.**', '*****@*****.**'], 'urn:oid:2.5.4.42': ['f_name'], 'urn:oid:0.9.2342.19200300.100.1.1': [unique_identifier], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.1': ['Member'], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10': [None], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7': [system_role.value], 'urn:oid:2.5.4.4': ['l_name'], 'urn:oid:2.5.4.3': ['Me Myself And I'], 'urn:oid:2.5.4.20': ['555-5555'], 'urn:oid:2.16.840.1.113730.3.1.3': ["student1"] if system_role == SystemRole.student else [], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6': ['*****@*****.**'] } rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.saml)\ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.system_role, SystemRole.student) self.assertIsNone(third_party_user.user.firstname) self.assertIsNone(third_party_user.user.lastname) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") self.assertIsNone(third_party_user.user.email) self.assertIsNone(third_party_user.user.student_number) self.assertIsNone( third_party_user.user.global_unique_identifier) # used attributes and no valid instructor values self.app.config[ 'SAML_ATTRIBUTE_FIRST_NAME'] = 'urn:oid:2.5.4.42' self.app.config['SAML_ATTRIBUTE_LAST_NAME'] = 'urn:oid:2.5.4.4' self.app.config[ 'SAML_ATTRIBUTE_STUDENT_NUMBER'] = 'urn:oid:2.16.840.1.113730.3.1.3' self.app.config[ 'SAML_ATTRIBUTE_EMAIL'] = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6' self.app.config[ 'SAML_ATTRIBUTE_USER_ROLE'] = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7' self.app.config['SAML_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = 'puid' self.app.config['SAML_INSTRUCTOR_ROLE_VALUES'] = {} unique_identifier = system_role.value + "_with_used_attributes" response_mock.get_attributes.return_value = { 'urn:oid:1.3.6.1.4.1.5923.1.1.1.9': ['*****@*****.**', '*****@*****.**'], 'urn:oid:2.5.4.42': ['f_name'], 'urn:oid:0.9.2342.19200300.100.1.1': [unique_identifier], 'puid': [system_role.value + "_puid"], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.1': ['Member'], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10': [None], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7': [system_role.value], 'urn:oid:2.5.4.4': ['l_name'], 'urn:oid:2.5.4.3': ['Me Myself And I'], 'urn:oid:2.5.4.20': ['555-5555'], 'urn:oid:2.16.840.1.113730.3.1.3': ["student1"] if system_role == SystemRole.student else [], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6': ['*****@*****.**'], } rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.saml) \ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.system_role, SystemRole.student) self.assertEqual(third_party_user.user.firstname, 'f_name') self.assertEqual(third_party_user.user.lastname, 'l_name') self.assertEqual(third_party_user.user.email, '*****@*****.**') self.assertEqual( third_party_user.user.global_unique_identifier, system_role.value + "_puid") if system_role == SystemRole.student: self.assertEqual(third_party_user.user.student_number, 'student1') else: self.assertIsNone(third_party_user.user.student_number) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") # used attributes and valid instructor values unique_identifier = system_role.value + "_with_used_attributes2" response_mock.get_attributes.return_value = { 'urn:oid:1.3.6.1.4.1.5923.1.1.1.9': ['*****@*****.**', '*****@*****.**'], 'urn:oid:2.5.4.42': ['f_name'], 'urn:oid:0.9.2342.19200300.100.1.1': [unique_identifier], 'puid': [system_role.value + "_puid2"], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.1': ['Member'], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10': [None], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7': [system_role.value], 'urn:oid:2.5.4.4': ['l_name'], 'urn:oid:2.5.4.3': ['Me Myself And I'], 'urn:oid:2.5.4.20': ['555-5555'], 'urn:oid:2.16.840.1.113730.3.1.3': ["student2"] if system_role == SystemRole.student else [], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6': ['*****@*****.**'] } self.app.config['SAML_INSTRUCTOR_ROLE_VALUES'] = { SystemRole.sys_admin.value, SystemRole.instructor.value } rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) third_party_user = ThirdPartyUser.query \ .filter_by(unique_identifier=unique_identifier, third_party_type=ThirdPartyType.saml)\ .one() self.assertIsNotNone(third_party_user) self.assertIsNotNone(third_party_user.user) self.assertEqual(third_party_user.user.firstname, 'f_name') self.assertEqual(third_party_user.user.lastname, 'l_name') self.assertEqual(third_party_user.user.email, '*****@*****.**') self.assertEqual( third_party_user.user.global_unique_identifier, system_role.value + "_puid2") if system_role == SystemRole.student: self.assertEqual(third_party_user.user.system_role, SystemRole.student) six.assertRegex(self, third_party_user.user.displayname, r"^Student_\d{8}") self.assertEqual(third_party_user.user.student_number, 'student2') else: self.assertEqual(third_party_user.user.system_role, SystemRole.instructor) self.assertEqual(third_party_user.user.displayname, "f_name l_name") self.assertIsNone(third_party_user.user.student_number) # test login existing user for system_role in [ SystemRole.student, SystemRole.instructor, SystemRole.sys_admin ]: self.app.config['SAML_ATTRIBUTE_FIRST_NAME'] = None self.app.config['SAML_ATTRIBUTE_LAST_NAME'] = None self.app.config['SAML_ATTRIBUTE_STUDENT_NUMBER'] = None self.app.config['SAML_ATTRIBUTE_EMAIL'] = None self.app.config['SAML_ATTRIBUTE_USER_ROLE'] = None self.app.config['SAML_INSTRUCTOR_ROLE_VALUES'] = {} self.app.config['SAML_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = None user = self.data.create_user(system_role) third_party_user = auth_data.create_third_party_user( user=user, third_party_type=ThirdPartyType.saml) original_firstname = user.firstname original_lastname = user.lastname original_email = user.email original_student_number = user.student_number new_student_number = original_student_number + "123" if user.student_number else None response_mock = mock.MagicMock() response_mock.__errors = [] response_mock.is_authenticated.return_value = True response_mock.get_attributes.return_value = { 'urn:oid:0.9.2342.19200300.100.1.1': [third_party_user.unique_identifier] } response_mock.get_nameid.return_value = "saml_mock_nameid" response_mock.get_session_index.return_value = "saml_session_index" with mock.patch('compair.api.login.get_saml_auth_response', return_value=response_mock): # test saml login disabled self.app.config['SAML_LOGIN_ENABLED'] = False rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert403(rv) # test saml login enabled self.app.config['SAML_LOGIN_ENABLED'] = True rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) # test overwrite disabled with no attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = True self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = True rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) # test overwrite enabled with no attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = False self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = False rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) # test overwrite enabled with no attributes self.app.config[ 'SAML_ATTRIBUTE_FIRST_NAME'] = 'urn:oid:2.5.4.42' self.app.config['SAML_ATTRIBUTE_LAST_NAME'] = 'urn:oid:2.5.4.4' self.app.config[ 'SAML_ATTRIBUTE_STUDENT_NUMBER'] = 'urn:oid:2.16.840.1.113730.3.1.3' self.app.config[ 'SAML_ATTRIBUTE_EMAIL'] = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6' self.app.config['SAML_GLOBAL_UNIQUE_IDENTIFIER_FIELD'] = 'puid' rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) response_mock.get_attributes.return_value = { 'urn:oid:1.3.6.1.4.1.5923.1.1.1.9': ['*****@*****.**', '*****@*****.**'], 'urn:oid:2.5.4.42': ['f_name'], 'urn:oid:0.9.2342.19200300.100.1.1': [third_party_user.unique_identifier], 'puid': ["should_be_ignored_since_already_linked"], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.1': ['Member', 'Staff'], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10': [None], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7': ['urn:mace:dir:entitlement:common-lib-terms'], 'urn:oid:2.5.4.4': ['l_name'], 'urn:oid:2.5.4.3': ['Me Myself And I'], 'urn:oid:2.5.4.20': ['555-5555'], 'urn:oid:2.16.840.1.113730.3.1.3': [new_student_number], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6': ['*****@*****.**'] } # test overwrite disabled with attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = True self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = True self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = True rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) # test overwrite enabled with attributes self.app.config['ALLOW_STUDENT_CHANGE_DISPLAY_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_NAME'] = False self.app.config['ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'] = False self.app.config['ALLOW_STUDENT_CHANGE_EMAIL'] = False rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) if system_role == SystemRole.student: self.assertEqual(user.firstname, 'f_name') self.assertEqual(user.lastname, 'l_name') self.assertEqual(user.email, '*****@*****.**') self.assertEqual(user.student_number, new_student_number) else: self.assertEqual(user.firstname, original_firstname) self.assertEqual(user.lastname, original_lastname) self.assertEqual(user.email, original_email) self.assertEqual(user.student_number, original_student_number) self.assertIsNone(user.global_unique_identifier) self.app.config[ 'SAML_ATTRIBUTE_USER_ROLE'] = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7' self.app.config['SAML_INSTRUCTOR_ROLE_VALUES'] = { SystemRole.instructor.value } # test automatic upgrading of system role for existing accounts for third_party_system_role in [ SystemRole.student, SystemRole.instructor, SystemRole.sys_admin ]: user = self.data.create_user(system_role) third_party_user = auth_data.create_third_party_user( user=user, third_party_type=ThirdPartyType.saml) db.session.commit() response_mock.get_attributes.return_value = { 'urn:oid:0.9.2342.19200300.100.1.1': [third_party_user.unique_identifier], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7': [third_party_system_role.value] } with mock.patch('compair.api.login.get_saml_auth_response', return_value=response_mock): rv = self.client.post('/api/saml/auth', follow_redirects=True) self.assert200(rv) # compair user system role will upgrade if system_role == SystemRole.student: # cannot upgrade to admin if third_party_system_role == SystemRole.instructor: self.assertEqual(user.system_role, SystemRole.instructor) else: self.assertEqual(user.system_role, SystemRole.student) elif system_role == SystemRole.instructor: # cannot upgrade to admin and shouldn't downgrade to student self.assertEqual(user.system_role, SystemRole.instructor) elif system_role == SystemRole.sys_admin: # shouldn't downgrade self.assertEqual(user.system_role, SystemRole.sys_admin) def test_saml_metadata(self): # test saml login enabled self.app.config['SAML_LOGIN_ENABLED'] = True self.app.config['SAML_EXPOSE_METADATA_ENDPOINT'] = True rv = self.client.get('/api/saml/metadata', headers={'Accept': 'text/xml'}) self.assert200(rv) # test saml login disabled self.app.config['SAML_LOGIN_ENABLED'] = False self.app.config['SAML_EXPOSE_METADATA_ENDPOINT'] = True rv = self.client.get('/api/saml/metadata', headers={'Accept': 'text/xml'}) self.assert404(rv) # test saml metadata disabled self.app.config['SAML_LOGIN_ENABLED'] = True self.app.config['SAML_EXPOSE_METADATA_ENDPOINT'] = False rv = self.client.get('/api/saml/metadata', headers={'Accept': 'text/xml'}) self.assert404(rv)
def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.expected_caliper_course = { 'academicSession': self.course.term, 'dateCreated': self.course.created.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateModified': self.course.modified.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'id': "https://localhost:8888/app/course/" + self.course.uuid, 'name': self.course.name, 'type': 'CourseOffering', 'otherIdentifiers': [{ 'identifier': self.lti_context.context_id, 'identifierType': 'LtiContextId', 'type': 'SystemIdentifier', 'extensions': { 'lis_course_offering_sourcedid': 'sis_course_id', 'lis_course_section_sourcedid': 'sis_section_id', 'oauth_consumer_key': self.lti_data.lti_consumer.oauth_consumer_key, }, }] } self.expected_caliper_assignment = { 'name': self.assignment.name, 'type': 'Assessment', 'dateCreated': self.assignment.created.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateModified': self.assignment.modified.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateToStartOn': self.assignment.answer_start.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid, 'isPartOf': self.expected_caliper_course, 'items': [{ 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/1", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/1", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/2", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/2", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/3", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/4", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/3", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/5", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/6", 'type': 'AssessmentItem' }], } self.expected_xapi_course = { 'id': "https://localhost:8888/app/course/" + self.course.uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/course', 'name': { 'en-US': self.course.name } }, 'objectType': 'Activity' } self.expected_xapi_assignment = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/assessment', 'name': { 'en-US': self.assignment.name }, 'description': { 'en-US': self.assignment.description }, }, 'objectType': 'Activity' }
class FileXAPITests(ComPAIRXAPITestCase): def setUp(self): super(ComPAIRXAPITestCase, self).setUp() self.data = SimpleAssignmentTestData() self.user = self.data.authorized_student self.assignment = self.data.assignments[0] def test_on_get_file(self): # not report or attachment on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="none", file_name="some_file" ) statements = self.get_and_clear_statement_log() self.assertEqual(len(statements), 0) # test report on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="report", file_name="some_report.csv" ) statements = self.get_and_clear_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0]['actor'], self.get_compair_actor(self.user)) self.assertEqual(statements[0]['verb'], { 'id': 'http://id.tincanapi.com/verb/downloaded', 'display': {'en-US': 'downloaded'} }) self.assertEqual(statements[0]['object'], { 'id': 'https://localhost:8888/app/report/some_report.csv', 'definition': {'type': 'http://activitystrea.ms/schema/1.0/file', 'name': {'en-US': 'Report'}}, 'objectType': 'Activity' }) self.assertNotIn('result', statements[0]) self.assertNotIn('context', statements[0]) # test attachment without file record on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name="some_file" ) statements = self.get_and_clear_statement_log() self.assertEqual(len(statements), 0) # test attachment file record (not linked to assignments or answers) file_record = self.data.create_file(self.user) on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name ) statements = self.get_and_clear_statement_log() self.assertEqual(len(statements), 0) # test attachment file record (assignment) self.assignment.file = file_record db.session.commit() on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name ) statements = self.get_and_clear_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0]['actor'], self.get_compair_actor(self.user)) self.assertEqual(statements[0]['verb'], { 'id': 'http://id.tincanapi.com/verb/downloaded', 'display': {'en-US': 'downloaded'} }) self.assertEqual(statements[0]['object'], { 'id': 'https://localhost:8888/app/attachment/'+file_record.name, 'definition': {'type': 'http://activitystrea.ms/schema/1.0/file', 'name': {'en-US': 'Assignment attachment'}}, 'objectType': 'Activity' }) self.assertNotIn('result', statements[0]) self.assertEqual(statements[0]['context'], { 'contextActivities': { 'parent': [ {'id': 'https://localhost:8888/app/xapi/assignment/'+self.assignment.uuid, 'objectType': 'Activity'} ], 'grouping': [ {'id': 'https://localhost:8888/app/xapi/course/'+self.data.main_course.uuid, 'objectType': 'Activity'} ] } }) # test attachment file record (answer) self.assignment.file = None answer = AnswerFactory( assignment=self.assignment, user=self.user, file=file_record ) db.session.commit() on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name ) statements = self.get_and_clear_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0]['actor'], self.get_compair_actor(self.user)) self.assertEqual(statements[0]['verb'], { 'id': 'http://id.tincanapi.com/verb/downloaded', 'display': {'en-US': 'downloaded'} }) self.assertEqual(statements[0]['object'], { 'id': 'https://localhost:8888/app/attachment/'+file_record.name, 'definition': {'type': 'http://activitystrea.ms/schema/1.0/file', 'name': {'en-US': 'Assignment answer attachment'}}, 'objectType': 'Activity' }) self.assertNotIn('result', statements[0]) self.assertEqual(statements[0]['context'], { 'contextActivities': { 'parent': [ {'id': 'https://localhost:8888/app/xapi/answer/'+answer.uuid, 'objectType': 'Activity'} ], 'grouping': [ {'id': 'https://localhost:8888/app/xapi/course/'+self.data.main_course.uuid, 'objectType': 'Activity'}, {'id': 'https://localhost:8888/app/xapi/assignment/'+self.assignment.uuid, 'objectType': 'Activity'}, {'id': 'https://localhost:8888/app/xapi/assignment/'+self.assignment.uuid+'/question', 'objectType': 'Activity'} ] } })
def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.answer = AnswerFactory(assignment=self.assignment, user=self.user) db.session.commit() self.expected_caliper_course = { 'academicSession': self.course.term, 'dateCreated': self.course.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.course.modified.replace(tzinfo=pytz.utc).isoformat(), 'id': "https://localhost:8888/app/course/" + self.course.uuid, 'name': self.course.name, 'type': 'CourseOffering', 'extensions': { 'ltiContexts': [{ 'context_id': self.lti_context.context_id, 'oauth_consumer_key': self.lti_data.lti_consumer.oauth_consumer_key, 'lis_course_offering_sourcedid': "sis_course_id", 'lis_course_section_sourcedid': "sis_section_id", }] } } self.expected_caliper_assignment = { 'name': self.assignment.name, 'type': 'Assessment', 'dateCreated': self.assignment.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.assignment.modified.replace(tzinfo=pytz.utc).isoformat(), 'dateToStartOn': self.assignment.answer_start.replace(tzinfo=pytz.utc).isoformat(), 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid, 'isPartOf': self.expected_caliper_course, 'items': [{ 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/1", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/1", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/2", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/2", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/3", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/4", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/3", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/5", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/6", 'type': 'AssessmentItem' }], } self.expected_caliper_assignment_question = { 'name': self.assignment.name, 'type': 'AssessmentItem', 'dateCreated': self.assignment.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.assignment.modified.replace(tzinfo=pytz.utc).isoformat(), 'dateToStartOn': self.assignment.answer_start.replace(tzinfo=pytz.utc).isoformat(), 'dateToSubmit': self.assignment.answer_end.replace(tzinfo=pytz.utc).isoformat(), 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question", 'isPartOf': self.expected_caliper_assignment, } self.expected_caliper_answer_attempt = { 'assignable': self.expected_caliper_assignment_question, 'assignee': self.get_compair_caliper_actor(self.user), 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question/attempt/" + self.answer.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': self.answer.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' } self.expected_caliper_answer = { 'attempt': self.expected_caliper_answer_attempt, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer.uuid, 'type': 'Response', 'dateCreated': self.answer.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.answer.modified.replace(tzinfo=pytz.utc).isoformat(), 'extensions': { 'characterCount': len(self.answer.content), 'content': self.answer.content, 'isDraft': False, 'wordCount': 8, } } self.expected_xapi_course = { 'id': "https://localhost:8888/app/course/" + self.course.uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/course', 'name': { 'en-US': self.course.name } }, 'objectType': 'Activity' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid + '/section/' + self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } self.expected_xapi_assignment = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/assessment', 'name': { 'en-US': self.assignment.name }, 'description': { 'en-US': self.assignment.description }, }, 'objectType': 'Activity' } self.expected_xapi_assignment_question = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question", 'definition': { 'type': 'http://adlnet.gov/expapi/activities/question', 'name': { 'en-US': self.assignment.name }, 'description': { 'en-US': self.assignment.description }, }, 'objectType': 'Activity' } self.expected_xapi_answer = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer.uuid, 'definition': { 'type': 'http://id.tincanapi.com/activitytype/solution', 'extensions': { 'http://id.tincanapi.com/extension/isDraft': False } }, 'objectType': 'Activity' }
def setUp(self): super(CoursesLTIAPITests, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData()
class FileXAPITests(ComPAIRXAPITestCase): def setUp(self): super(ComPAIRXAPITestCase, self).setUp() self.data = SimpleAssignmentTestData() self.user = self.data.authorized_student self.assignment = self.data.assignments[0] def test_on_get_file(self): # not report or attachment on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="none", file_name="some_file") statements = self.get_and_clear_statement_log() self.assertEqual(len(statements), 0) # test report on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="report", file_name="some_report.csv") statements = self.get_and_clear_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0]['actor'], self.get_compair_actor(self.user)) self.assertEqual( statements[0]['verb'], { 'id': 'http://id.tincanapi.com/verb/downloaded', 'display': { 'en-US': 'downloaded' } }) self.assertEqual( statements[0]['object'], { 'id': 'https://localhost:8888/app/report/some_report.csv', 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': { 'en-US': 'Report' } }, 'objectType': 'Activity' }) self.assertNotIn('result', statements[0]) self.assertNotIn('context', statements[0]) # test attachment without file record on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name="some_file") statements = self.get_and_clear_statement_log() self.assertEqual(len(statements), 0) # test attachment file record (not linked to assignments or answers) file_record = self.data.create_file(self.user) on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name) statements = self.get_and_clear_statement_log() self.assertEqual(len(statements), 0) # test attachment file record (assignment) self.assignment.file = file_record db.session.commit() on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name) statements = self.get_and_clear_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0]['actor'], self.get_compair_actor(self.user)) self.assertEqual( statements[0]['verb'], { 'id': 'http://id.tincanapi.com/verb/downloaded', 'display': { 'en-US': 'downloaded' } }) self.assertEqual( statements[0]['object'], { 'id': 'https://localhost:8888/app/attachment/' + file_record.name, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': { 'en-US': 'Assignment attachment' } }, 'objectType': 'Activity' }) self.assertNotIn('result', statements[0]) self.assertEqual( statements[0]['context'], { 'contextActivities': { 'parent': [{ 'id': 'https://localhost:8888/app/xapi/assignment/' + self.assignment.uuid, 'objectType': 'Activity' }], 'grouping': [{ 'id': 'https://localhost:8888/app/xapi/course/' + self.data.main_course.uuid, 'objectType': 'Activity' }] } }) # test attachment file record (answer) self.assignment.file = None answer = AnswerFactory(assignment=self.assignment, user=self.user, file=file_record) db.session.commit() on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name) statements = self.get_and_clear_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0]['actor'], self.get_compair_actor(self.user)) self.assertEqual( statements[0]['verb'], { 'id': 'http://id.tincanapi.com/verb/downloaded', 'display': { 'en-US': 'downloaded' } }) self.assertEqual( statements[0]['object'], { 'id': 'https://localhost:8888/app/attachment/' + file_record.name, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': { 'en-US': 'Assignment answer attachment' } }, 'objectType': 'Activity' }) self.assertNotIn('result', statements[0]) self.assertEqual( statements[0]['context'], { 'contextActivities': { 'parent': [{ 'id': 'https://localhost:8888/app/xapi/answer/' + answer.uuid, 'objectType': 'Activity' }], 'grouping': [{ 'id': 'https://localhost:8888/app/xapi/course/' + self.data.main_course.uuid, 'objectType': 'Activity' }, { 'id': 'https://localhost:8888/app/xapi/assignment/' + self.assignment.uuid, 'objectType': 'Activity' }, { 'id': 'https://localhost:8888/app/xapi/assignment/' + self.assignment.uuid + '/question', 'objectType': 'Activity' }] } })
class FileLearningRecordTests(ComPAIRLearningRecordTestCase): def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.answer = AnswerFactory(assignment=self.assignment, user=self.user) db.session.commit() self.expected_caliper_course = { 'academicSession': self.course.term, 'dateCreated': self.course.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.course.modified.replace(tzinfo=pytz.utc).isoformat(), 'id': "https://localhost:8888/app/course/" + self.course.uuid, 'name': self.course.name, 'type': 'CourseOffering', 'extensions': { 'ltiContexts': [{ 'context_id': self.lti_context.context_id, 'oauth_consumer_key': self.lti_data.lti_consumer.oauth_consumer_key, 'lis_course_offering_sourcedid': "sis_course_id", 'lis_course_section_sourcedid': "sis_section_id", }] } } self.expected_caliper_assignment = { 'name': self.assignment.name, 'type': 'Assessment', 'dateCreated': self.assignment.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.assignment.modified.replace(tzinfo=pytz.utc).isoformat(), 'dateToStartOn': self.assignment.answer_start.replace(tzinfo=pytz.utc).isoformat(), 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid, 'isPartOf': self.expected_caliper_course, 'items': [{ 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/1", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/1", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/2", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/2", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/3", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/4", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/3", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/5", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/6", 'type': 'AssessmentItem' }], } self.expected_caliper_assignment_question = { 'name': self.assignment.name, 'type': 'AssessmentItem', 'dateCreated': self.assignment.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.assignment.modified.replace(tzinfo=pytz.utc).isoformat(), 'dateToStartOn': self.assignment.answer_start.replace(tzinfo=pytz.utc).isoformat(), 'dateToSubmit': self.assignment.answer_end.replace(tzinfo=pytz.utc).isoformat(), 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question", 'isPartOf': self.expected_caliper_assignment, } self.expected_caliper_answer_attempt = { 'assignable': self.expected_caliper_assignment_question, 'assignee': self.get_compair_caliper_actor(self.user), 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question/attempt/" + self.answer.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': self.answer.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' } self.expected_caliper_answer = { 'attempt': self.expected_caliper_answer_attempt, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer.uuid, 'type': 'Response', 'dateCreated': self.answer.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.answer.modified.replace(tzinfo=pytz.utc).isoformat(), 'extensions': { 'characterCount': len(self.answer.content), 'content': self.answer.content, 'isDraft': False, 'wordCount': 8, } } self.expected_xapi_course = { 'id': "https://localhost:8888/app/course/" + self.course.uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/course', 'name': { 'en-US': self.course.name } }, 'objectType': 'Activity' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid + '/section/' + self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } self.expected_xapi_assignment = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/assessment', 'name': { 'en-US': self.assignment.name }, 'description': { 'en-US': self.assignment.description }, }, 'objectType': 'Activity' } self.expected_xapi_assignment_question = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question", 'definition': { 'type': 'http://adlnet.gov/expapi/activities/question', 'name': { 'en-US': self.assignment.name }, 'description': { 'en-US': self.assignment.description }, }, 'objectType': 'Activity' } self.expected_xapi_answer = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer.uuid, 'definition': { 'type': 'http://id.tincanapi.com/activitytype/solution', 'extensions': { 'http://id.tincanapi.com/extension/isDraft': False } }, 'objectType': 'Activity' } def test_on_get_file(self): # not report or attachment on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="none", file_name="some_file") events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 0) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 0) # test report on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="report", file_name="some_report.csv") expected_caliper_object = { "id": 'https://localhost:8888/app/report/some_report.csv', "type": "Document", "name": "some_report.csv", "mediaType": "text/csv" } expected_caliper_event = { 'action': 'Viewed', 'actor': self.get_compair_caliper_actor(self.user), 'object': expected_caliper_object, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'ViewEvent' } events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_object = { 'id': 'https://localhost:8888/app/report/some_report.csv', 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': { 'en-US': 'some_report.csv' }, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': "text/csv" } }, 'objectType': 'Activity' } expected_xapi_verb = { 'id': 'http://id.tincanapi.com/verb/downloaded', 'display': { 'en-US': 'downloaded' } } expected_xapi_context = { 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_xapi_verb, "object": expected_xapi_object, "context": expected_xapi_context } statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # test attachment without file record on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name="some_file") events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 0) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 0) # test attachment file record (not linked to assignments or answers) file_record = self.data.create_file(self.user) on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 0) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 0) # test attachment file record (assignment) self.assignment.file_id = file_record.id db.session.commit() on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name) expected_caliper_object = { "id": 'https://localhost:8888/app/attachment/' + file_record.name, "type": "Document", "name": file_record.alias, "mediaType": 'application/pdf', "isPartOf": self.expected_caliper_assignment, "dateCreated": file_record.created.replace(tzinfo=pytz.utc).isoformat(), "dateModified": file_record.modified.replace(tzinfo=pytz.utc).isoformat() } self.expected_caliper_assignment[ 'dateModified'] = self.assignment.modified.replace( tzinfo=pytz.utc).isoformat() expected_caliper_event['object'] = expected_caliper_object expected_caliper_event['membership'] = self.get_caliper_membership( self.course, self.user, self.lti_context) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_object = { 'id': 'https://localhost:8888/app/attachment/' + file_record.name, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': { 'en-US': file_record.alias }, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': 'application/pdf' } }, 'objectType': 'Activity' } expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [ self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['object'] = expected_xapi_object expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # test attachment file record (answer) self.assignment.file_id = None self.answer.file_id = file_record.id db.session.commit() on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name) self.expected_caliper_assignment_question[ 'dateModified'] = self.assignment.modified.replace( tzinfo=pytz.utc).isoformat() self.expected_caliper_assignment[ 'dateModified'] = self.assignment.modified.replace( tzinfo=pytz.utc).isoformat() self.expected_caliper_answer[ 'dateModified'] = self.answer.modified.replace( tzinfo=pytz.utc).isoformat() expected_caliper_object["isPartOf"] = self.expected_caliper_answer events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_answer], 'grouping': [ self.expected_xapi_assignment_question, self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) def test_on_attach_file(self): file_record = self.data.create_file(self.user) self.assignment.file_id = file_record.id db.session.commit() # attache to assignment on_attach_file.send( current_app._get_current_object(), event_name=on_attach_file.name, user=self.user, file=file_record, ) expected_caliper_object = { "id": 'https://localhost:8888/app/attachment/' + file_record.name, "type": "Document", "name": file_record.alias, "mediaType": 'application/pdf', "isPartOf": self.expected_caliper_assignment, "dateCreated": file_record.created.replace(tzinfo=pytz.utc).isoformat(), "dateModified": file_record.modified.replace(tzinfo=pytz.utc).isoformat() } self.expected_caliper_assignment[ 'dateModified'] = self.assignment.modified.replace( tzinfo=pytz.utc).isoformat() expected_caliper_event = { 'action': 'Attached', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': expected_caliper_object, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'Event' } events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_verb = { 'id': 'http://activitystrea.ms/schema/1.0/attach', 'display': { 'en-US': 'attached' } } expected_xapi_object = { 'id': 'https://localhost:8888/app/attachment/' + file_record.name, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': { 'en-US': file_record.alias }, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': 'application/pdf' } }, 'objectType': 'Activity' } expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [ self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_xapi_verb, "object": expected_xapi_object, "context": expected_xapi_context } statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # attach to answer self.assignment.file_id = None self.answer.file_id = file_record.id db.session.commit() on_attach_file.send( current_app._get_current_object(), event_name=on_attach_file.name, user=self.user, file=file_record, ) self.expected_caliper_assignment_question[ 'dateModified'] = self.assignment.modified.replace( tzinfo=pytz.utc).isoformat() self.expected_caliper_assignment[ 'dateModified'] = self.assignment.modified.replace( tzinfo=pytz.utc).isoformat() self.expected_caliper_answer[ 'dateModified'] = self.answer.modified.replace( tzinfo=pytz.utc).isoformat() expected_caliper_object["isPartOf"] = self.expected_caliper_answer events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_answer], 'grouping': [ self.expected_xapi_assignment_question, self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) def test_on_detach_file(self): file_record = self.data.create_file(self.user) db.session.commit() # attache to assignment on_detach_file.send(current_app._get_current_object(), event_name=on_detach_file.name, user=self.user, file=file_record, assignment=self.assignment) expected_caliper_object = { "id": 'https://localhost:8888/app/attachment/' + file_record.name, "type": "Document", "name": file_record.alias, "mediaType": 'application/pdf', "isPartOf": self.expected_caliper_assignment, "dateCreated": file_record.created.replace(tzinfo=pytz.utc).isoformat(), "dateModified": file_record.modified.replace(tzinfo=pytz.utc).isoformat() } expected_caliper_event = { 'action': 'Removed', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': expected_caliper_object, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'Event' } events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_verb = { 'id': 'http://activitystrea.ms/schema/1.0/delete', 'display': { 'en-US': 'deleted' } } expected_xapi_object = { 'id': 'https://localhost:8888/app/attachment/' + file_record.name, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': { 'en-US': file_record.alias }, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': 'application/pdf' } }, 'objectType': 'Activity' } expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [ self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_xapi_verb, "object": expected_xapi_object, "context": expected_xapi_context } statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # attach to answer on_detach_file.send(current_app._get_current_object(), event_name=on_detach_file.name, user=self.user, file=file_record, answer=self.answer) expected_caliper_object["isPartOf"] = self.expected_caliper_answer events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_answer], 'grouping': [ self.expected_xapi_assignment_question, self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement)
class FileLearningRecordTests(ComPAIRLearningRecordTestCase): def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.answer = AnswerFactory( assignment=self.assignment, user=self.user ) db.session.commit() self.expected_caliper_course = { 'academicSession': self.course.term, 'dateCreated': self.course.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.course.modified.replace(tzinfo=pytz.utc).isoformat(), 'id': "https://localhost:8888/app/course/"+self.course.uuid, 'name': self.course.name, 'type': 'CourseOffering', 'extensions': { 'ltiContexts': [{ 'context_id': self.lti_context.context_id, 'oauth_consumer_key': self.lti_data.lti_consumer.oauth_consumer_key, 'lis_course_offering_sourcedid': "sis_course_id", 'lis_course_section_sourcedid': "sis_section_id", }] } } self.expected_caliper_assignment = { 'name': self.assignment.name, 'type': 'Assessment', 'dateCreated': self.assignment.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.assignment.modified.replace(tzinfo=pytz.utc).isoformat(), 'dateToStartOn': self.assignment.answer_start.replace(tzinfo=pytz.utc).isoformat(), 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid, 'isPartOf': self.expected_caliper_course, 'items': [{ 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/question/1", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/evaluation/question/1", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/evaluation/question/2", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/question/2", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/evaluation/question/3", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/evaluation/question/4", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/question/3", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/evaluation/question/5", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/evaluation/question/6", 'type': 'AssessmentItem' }], } self.expected_caliper_assignment_question = { 'name': self.assignment.name, 'type': 'AssessmentItem', 'dateCreated': self.assignment.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.assignment.modified.replace(tzinfo=pytz.utc).isoformat(), 'dateToStartOn': self.assignment.answer_start.replace(tzinfo=pytz.utc).isoformat(), 'dateToSubmit': self.assignment.answer_end.replace(tzinfo=pytz.utc).isoformat(), 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question", 'isPartOf': self.expected_caliper_assignment, } self.expected_caliper_answer_attempt = { 'assignable': self.expected_caliper_assignment_question, 'assignee': self.get_compair_caliper_actor(self.user), 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question/attempt/"+self.answer.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': self.answer.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' } self.expected_caliper_answer = { 'attempt': self.expected_caliper_answer_attempt, 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer.uuid, 'type': 'Response', 'dateCreated': self.answer.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.answer.modified.replace(tzinfo=pytz.utc).isoformat(), 'extensions': { 'characterCount': len(self.answer.content), 'content': self.answer.content, 'isDraft': False, 'wordCount': 8, } } self.expected_xapi_course = { 'id': "https://localhost:8888/app/course/"+self.course.uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/course', 'name': {'en-US': self.course.name} }, 'objectType': 'Activity' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid+'/section/'+self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } self.expected_xapi_assignment = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/assessment', 'name': {'en-US': self.assignment.name}, 'description': {'en-US': self.assignment.description}, }, 'objectType': 'Activity' } self.expected_xapi_assignment_question = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question", 'definition': { 'type': 'http://adlnet.gov/expapi/activities/question', 'name': {'en-US': self.assignment.name}, 'description': {'en-US': self.assignment.description}, }, 'objectType': 'Activity' } self.expected_xapi_answer = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer.uuid, 'definition': { 'type': 'http://id.tincanapi.com/activitytype/solution', 'extensions': { 'http://id.tincanapi.com/extension/isDraft': False } }, 'objectType': 'Activity' } def test_on_get_file(self): # not report or attachment on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="none", file_name="some_file" ) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 0) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 0) # test report on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="report", file_name="some_report.csv" ) expected_caliper_object = { "id": 'https://localhost:8888/app/report/some_report.csv', "type": "Document", "name": "some_report.csv", "mediaType": "text/csv" } expected_caliper_event = { 'action': 'Viewed', 'actor': self.get_compair_caliper_actor(self.user), 'object': expected_caliper_object, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'ViewEvent' } events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_object = { 'id': 'https://localhost:8888/app/report/some_report.csv', 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': {'en-US': 'some_report.csv'}, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': "text/csv" } }, 'objectType': 'Activity' } expected_xapi_verb = { 'id': 'http://id.tincanapi.com/verb/downloaded', 'display': {'en-US': 'downloaded'} } expected_xapi_context = { 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_xapi_verb, "object": expected_xapi_object, "context": expected_xapi_context } statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # test attachment without file record on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name="some_file" ) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 0) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 0) # test attachment file record (not linked to assignments or answers) file_record = self.data.create_file(self.user) on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name ) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 0) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 0) # test attachment file record (assignment) self.assignment.file_id = file_record.id db.session.commit() on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name ) expected_caliper_object = { "id": 'https://localhost:8888/app/attachment/'+file_record.name, "type": "Document", "name": file_record.alias, "mediaType": 'application/pdf', "isPartOf": self.expected_caliper_assignment, "dateCreated": file_record.created.replace(tzinfo=pytz.utc).isoformat(), "dateModified": file_record.modified.replace(tzinfo=pytz.utc).isoformat() } self.expected_caliper_assignment['dateModified'] = self.assignment.modified.replace(tzinfo=pytz.utc).isoformat() expected_caliper_event['object'] = expected_caliper_object expected_caliper_event['membership'] = self.get_caliper_membership(self.course, self.user, self.lti_context) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_object = { 'id': 'https://localhost:8888/app/attachment/'+file_record.name, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': {'en-US': file_record.alias}, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': 'application/pdf' } }, 'objectType': 'Activity' } expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['object'] = expected_xapi_object expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # test attachment file record (answer) self.assignment.file_id = None self.answer.file_id = file_record.id db.session.commit() on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name ) self.expected_caliper_assignment_question['dateModified'] = self.assignment.modified.replace(tzinfo=pytz.utc).isoformat() self.expected_caliper_assignment['dateModified'] = self.assignment.modified.replace(tzinfo=pytz.utc).isoformat() self.expected_caliper_answer['dateModified'] = self.answer.modified.replace(tzinfo=pytz.utc).isoformat() expected_caliper_object["isPartOf"] = self.expected_caliper_answer events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_answer], 'grouping': [self.expected_xapi_assignment_question, self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) def test_on_attach_file(self): file_record = self.data.create_file(self.user) self.assignment.file_id = file_record.id db.session.commit() # attache to assignment on_attach_file.send( current_app._get_current_object(), event_name=on_attach_file.name, user=self.user, file=file_record, ) expected_caliper_object = { "id": 'https://localhost:8888/app/attachment/'+file_record.name, "type": "Document", "name": file_record.alias, "mediaType": 'application/pdf', "isPartOf": self.expected_caliper_assignment, "dateCreated": file_record.created.replace(tzinfo=pytz.utc).isoformat(), "dateModified": file_record.modified.replace(tzinfo=pytz.utc).isoformat() } self.expected_caliper_assignment['dateModified'] = self.assignment.modified.replace(tzinfo=pytz.utc).isoformat() expected_caliper_event = { 'action': 'Attached', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': expected_caliper_object, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'Event' } events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_verb = { 'id': 'http://activitystrea.ms/schema/1.0/attach', 'display': {'en-US': 'attached'} } expected_xapi_object = { 'id': 'https://localhost:8888/app/attachment/'+file_record.name, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': {'en-US': file_record.alias}, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': 'application/pdf' } }, 'objectType': 'Activity' } expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_xapi_verb, "object": expected_xapi_object, "context": expected_xapi_context } statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # attach to answer self.assignment.file_id = None self.answer.file_id = file_record.id db.session.commit() on_attach_file.send( current_app._get_current_object(), event_name=on_attach_file.name, user=self.user, file=file_record, ) self.expected_caliper_assignment_question['dateModified'] = self.assignment.modified.replace(tzinfo=pytz.utc).isoformat() self.expected_caliper_assignment['dateModified'] = self.assignment.modified.replace(tzinfo=pytz.utc).isoformat() self.expected_caliper_answer['dateModified'] = self.answer.modified.replace(tzinfo=pytz.utc).isoformat() expected_caliper_object["isPartOf"] = self.expected_caliper_answer events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_answer], 'grouping': [self.expected_xapi_assignment_question, self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) def test_on_detach_file(self): file_record = self.data.create_file(self.user) db.session.commit() # attache to assignment on_detach_file.send( current_app._get_current_object(), event_name=on_detach_file.name, user=self.user, file=file_record, assignment=self.assignment ) expected_caliper_object = { "id": 'https://localhost:8888/app/attachment/'+file_record.name, "type": "Document", "name": file_record.alias, "mediaType": 'application/pdf', "isPartOf": self.expected_caliper_assignment, "dateCreated": file_record.created.replace(tzinfo=pytz.utc).isoformat(), "dateModified": file_record.modified.replace(tzinfo=pytz.utc).isoformat() } expected_caliper_event = { 'action': 'Removed', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': expected_caliper_object, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'Event' } events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_verb = { 'id': 'http://activitystrea.ms/schema/1.0/delete', 'display': {'en-US': 'deleted'} } expected_xapi_object = { 'id': 'https://localhost:8888/app/attachment/'+file_record.name, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': {'en-US': file_record.alias}, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': 'application/pdf' } }, 'objectType': 'Activity' } expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_xapi_verb, "object": expected_xapi_object, "context": expected_xapi_context } statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # attach to answer on_detach_file.send( current_app._get_current_object(), event_name=on_detach_file.name, user=self.user, file=file_record, answer=self.answer ) expected_caliper_object["isPartOf"] = self.expected_caliper_answer events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_answer], 'grouping': [self.expected_xapi_assignment_question, self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement)
def setUp(self): super(ComPAIRXAPITestCase, self).setUp() self.data = SimpleAssignmentTestData() self.user = self.data.authorized_student self.course = self.data.main_course self.assignment = self.data.assignments[0]
def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.answer = AnswerFactory( assignment=self.assignment, user=self.user ) db.session.commit() self.expected_caliper_course = { 'academicSession': self.course.term, 'dateCreated': self.course.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.course.modified.replace(tzinfo=pytz.utc).isoformat(), 'id': "https://localhost:8888/app/course/"+self.course.uuid, 'name': self.course.name, 'type': 'CourseOffering', 'extensions': { 'ltiContexts': [{ 'context_id': self.lti_context.context_id, 'oauth_consumer_key': self.lti_data.lti_consumer.oauth_consumer_key, 'lis_course_offering_sourcedid': "sis_course_id", 'lis_course_section_sourcedid': "sis_section_id", }] } } self.expected_caliper_assignment = { 'name': self.assignment.name, 'type': 'Assessment', 'dateCreated': self.assignment.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.assignment.modified.replace(tzinfo=pytz.utc).isoformat(), 'dateToStartOn': self.assignment.answer_start.replace(tzinfo=pytz.utc).isoformat(), 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid, 'isPartOf': self.expected_caliper_course, 'items': [{ 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/question/1", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/evaluation/question/1", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/evaluation/question/2", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/question/2", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/evaluation/question/3", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/evaluation/question/4", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/question/3", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/evaluation/question/5", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/evaluation/question/6", 'type': 'AssessmentItem' }], } self.expected_caliper_assignment_question = { 'name': self.assignment.name, 'type': 'AssessmentItem', 'dateCreated': self.assignment.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.assignment.modified.replace(tzinfo=pytz.utc).isoformat(), 'dateToStartOn': self.assignment.answer_start.replace(tzinfo=pytz.utc).isoformat(), 'dateToSubmit': self.assignment.answer_end.replace(tzinfo=pytz.utc).isoformat(), 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question", 'isPartOf': self.expected_caliper_assignment, } self.expected_caliper_answer_attempt = { 'assignable': self.expected_caliper_assignment_question, 'assignee': self.get_compair_caliper_actor(self.user), 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question/attempt/"+self.answer.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': self.answer.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' } self.expected_caliper_answer = { 'attempt': self.expected_caliper_answer_attempt, 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer.uuid, 'type': 'Response', 'dateCreated': self.answer.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.answer.modified.replace(tzinfo=pytz.utc).isoformat(), 'extensions': { 'characterCount': len(self.answer.content), 'content': self.answer.content, 'isDraft': False, 'wordCount': 8, } } self.expected_xapi_course = { 'id': "https://localhost:8888/app/course/"+self.course.uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/course', 'name': {'en-US': self.course.name} }, 'objectType': 'Activity' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid+'/section/'+self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } self.expected_xapi_assignment = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/assessment', 'name': {'en-US': self.assignment.name}, 'description': {'en-US': self.assignment.description}, }, 'objectType': 'Activity' } self.expected_xapi_assignment_question = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question", 'definition': { 'type': 'http://adlnet.gov/expapi/activities/question', 'name': {'en-US': self.assignment.name}, 'description': {'en-US': self.assignment.description}, }, 'objectType': 'Activity' } self.expected_xapi_answer = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer.uuid, 'definition': { 'type': 'http://id.tincanapi.com/activitytype/solution', 'extensions': { 'http://id.tincanapi.com/extension/isDraft': False } }, 'objectType': 'Activity' }