def test_delete(self): other, teammate, team, cycles, responses = self.create() # Forbidden to delete other people's responses. self.testapp.delete( '/api/responses/{}'.format(responses['user_team_other'].uid), headers=jwt_headers(teammate), status=403, ) # Successful to delete your own. self.testapp.delete( '/api/responses/{}'.format(responses['user_team_user1'].uid), headers=jwt_headers(teammate), status=204, ) self.assertIsNone(Response.get_by_id(responses['user_team_user1'].uid)) # Forbidden to delete other team's responses. self.testapp.delete( '/api/responses/{}'.format(responses['team_other'].uid), headers=jwt_headers(teammate), status=403, ) # Successful to delete responses from your team. self.testapp.delete( '/api/responses/{}'.format(responses['team_team'].uid), headers=jwt_headers(teammate), status=204, ) self.assertIsNone(Response.get_by_id(responses['team_team'].uid))
def test_update_question_level(self): """Question-level updates without conflicts.""" other, teammate, team, cycles, responses = self.create() # New question. Added to the existing body (existing remain). response_id = responses['team_team'].uid self.testapp.put_json( '/api/responses/{}'.format(response_id), {'body': { 'question_new': { 'value': 'foo', 'modified': None } }}, headers=jwt_headers(teammate), ) fetched = Response.get_by_id(response_id) self.assertIn('question', fetched.body) # New question was given a timestamp. self.assertIsNotNone(fetched.body['question_new']['modified']) # Question unchanged. Do nothing, even if timestamp is old. old_body = self.default_body() old_body['question']['modified'] = '2000-01-01T00:00:00Z' self.testapp.put_json( '/api/responses/{}'.format(response_id), {'body': old_body}, headers=jwt_headers(teammate), ) fetched = Response.get_by_id(response_id) self.assertEqual( fetched.body['question'], # current value is... self.default_body()['question'], # how it was originally created ) # Non-stale update. Existing timestamp matches and value changes. new_body = self.default_body() new_body['question']['value'] = 'updated' self.testapp.put_json( '/api/responses/{}'.format(response_id), {'body': new_body}, headers=jwt_headers(teammate), ) fetched = Response.get_by_id(response_id) self.assertEqual( fetched.body['question']['value'], new_body['question']['value'], ) # Timestamp should update. self.assertGreater( fetched.body['question']['modified'], new_body['question']['modified'], )
def test_update_force(self): """Force flag is set, override conflicts and save.""" other, teammate, team, cycles, responses = self.create() # Value is changed and timestamp is old response_id = responses['team_team'].uid now = datetime.datetime.now().strftime(config.iso_datetime_format) old_time = '2000-01-01T00:00:00Z' body = { # based on stale data, but will be accepted anyway 'question': { 'value': 'bar', 'modified': old_time }, # this should be accepted also 'question_new': { 'value': 'foo', 'modified': old_time }, } self.testapp.put_json( '/api/responses/{}?force=true'.format(response_id), {'body': body}, headers=jwt_headers(teammate), ) fetched = Response.get_by_id(response_id) for k, info in body.items(): self.assertIn(k, fetched.body) self.assertEqual(info['value'], fetched.body[k]['value']) self.assertTrue(fetched.body[k]['modified'] >= now)
def test_update_with_conflicts(self): other, teammate, team, cycles, responses = self.create() # Value is changed and timestamp is old response_id = responses['team_team'].uid self.testapp.put_json( '/api/responses/{}'.format(response_id), { 'body': { # based on stale data 'question': { 'value': 'bar', 'modified': '2000-01-01T00:00:00Z', }, # this should be ignored b/c of above 'question_new': { 'value': 'foo', 'modified': '2000-01-01T00:00:00Z', }, }, }, headers=jwt_headers(teammate), status=409, ) fetched = Response.get_by_id(response_id) # Whole update is rejected, body unchanged. self.assertEqual(responses['team_team'].body, fetched.body)
def test_update_simple(self): """Check ownership-based update permission, no conflicts.""" other, teammate, team, cycles, responses = self.create() new_body = self.default_body() new_body['question']['value'] = 'change' # Forbidden to update other people's responses. self.testapp.put_json( '/api/responses/{}'.format(responses['user_team_other'].uid), {'body': new_body}, headers=jwt_headers(teammate), status=403, ) # Successful to update your own. self.testapp.put_json( '/api/responses/{}'.format(responses['user_team_user1'].uid), {'body': new_body}, headers=jwt_headers(teammate), ) self.assertTrue( self.body_values_match( Response.get_by_id(responses['user_team_user1'].uid).body, new_body, )) # Forbidden to update other teams. self.testapp.put_json( '/api/responses/{}'.format(responses['team_other'].uid), {'body': new_body}, headers=jwt_headers(teammate), status=403, ) # Successful to update team-level. self.testapp.put_json( '/api/responses/{}'.format(responses['team_team'].uid), {'body': new_body}, headers=jwt_headers(teammate), ) self.assertTrue( self.body_values_match( Response.get_by_id(responses['team_team'].uid).body, new_body, ))
def test_update_privacy_ignored(self): other, teammate, team, cycles, responses = self.create() # Starts private. to_update = responses['user_team_user1'] self.assertTrue(to_update.private) # Try to make it non-private. self.testapp.put_json( '/api/responses/{}'.format(to_update.uid), dict(to_update.to_client_dict(), private=False), headers=jwt_headers(teammate), ) # Privacy unchanged. fetched = Response.get_by_id(to_update.uid) self.assertEqual(to_update.private, fetched.private)
def test_atomic_updates(self): """Not exactly a test; more of a proof of concept that mysql row locking works.""" r = Response.create( user_id='User_foo', team_id='Team_foo', parent_id='Cycle_foo', module_label='ModuleFoo', body={'foo': 'bar'}, progress=0, ) r.put() table = 'response' with mysql_connection.connect(retry_on_error=False) as sql: # This simulates one user clicking "submit" to a module. sql.select_row_for_update(table, 'uid', r.uid) # locks with self.assertRaises(MySQLdb.OperationalError): with mysql_connection.connect(retry_on_error=False) as sql: # This default to 50, which is too long to wait. sql.query('SET innodb_lock_wait_timeout = 1', tuple()) # This simulates another user clicking submit on their # client at the exact same time, which if it weren't for the # lock would be a race condition. Here it should just wait, # and then reach the timeout and raise. sql.update_row(table, 'uid', r.uid, progress=1) # Unfortunately exiting here will close _both_ connections, # so we'll have to let that happen and open a third. # If lock succeeded, the data should be unchanged. fetched = Response.get_by_id(r.uid) self.assertEqual(fetched.progress, 0)
def test_body_too_large(self): other, teammate, team, cycles, responses = self.create() response_params = { 'user_id': teammate.uid, 'team_id': team.uid, 'parent_id': cycles[1].uid, 'module_label': 'ModuleBar', } # A property is too long. self.testapp.post_json( '/api/responses', dict(response_params, body={'foo': { 'value': 'x' * 10**5 }}), headers=jwt_headers(teammate), status=413, ) # Too many properties. self.testapp.post_json( '/api/responses', dict( response_params, body={'foo{}'.format(x): { 'value': x } for x in range(10**3)}, ), headers=jwt_headers(teammate), status=413, ) # Both posts should have prevented new responses from being stored. self.assertEqual(len(Response.get()), len(responses)) # Successful POST resp = self.testapp.post_json( '/api/responses', dict(response_params, body={'safe': { 'value': 'data' }}), headers=jwt_headers(teammate), ) resp_dict = json.loads(resp.body) put_url = '/api/responses/{}'.format(resp_dict['uid']) # Same errors but for PUT # A property is too long. self.testapp.put_json( put_url, {'body': { 'foo': { 'value': 'x' * 10**5 } }}, headers=jwt_headers(teammate), status=413, ) # Too many properties. self.testapp.put_json( put_url, {'body': {'foo{}'.format(x): { 'value': x } for x in range(10**3)}}, headers=jwt_headers(teammate), status=413, ) # Puts should have left body unchanged. fetched_body = Response.get_by_id(resp_dict['uid']).body self.assertEqual(fetched_body.keys(), ['safe']) self.assertEqual(fetched_body['safe']['value'], 'data')
def test_create(self): other, teammate, team, cycles, responses = self.create() # Can't choose non-existant parent. self.testapp.post_json( '/api/responses', { 'team_id': team.uid, 'parent_id': 'Cycle_dne', 'module_label': 'ModuleFoo', }, headers=jwt_headers(teammate), status=403, ) # Can't choose an unowned parent. self.testapp.post_json( '/api/responses', { 'team_id': team.uid, 'parent_id': cycles[0].uid, 'module_label': 'ModuleFoo', }, headers=jwt_headers(other), status=403, ) # Can't create duplicate responses. self.testapp.post_json( '/api/responses', { 'team_id': team.uid, 'parent_id': cycles[0].uid, 'module_label': 'ModuleFoo', 'user_id': teammate.uid, }, headers=jwt_headers(teammate), status=409, ) # Can't create responses for other people. self.testapp.post_json( '/api/responses', { 'team_id': team.uid, 'parent_id': cycles[0].uid, 'module_label': 'ModuleFoo', 'user_id': teammate.uid, }, headers=jwt_headers(other), status=403, ) # Success for own team, user-level, private. response = self.testapp.post_json( '/api/responses', { 'user_id': teammate.uid, 'team_id': team.uid, 'parent_id': cycles[1].uid, 'module_label': 'ModuleBar', 'body': self.default_body(), }, headers=jwt_headers(teammate), ) user_fetched = Response.get_by_id(json.loads(response.body)['uid']) self.assertTrue(user_fetched.private) # Success for own team, user-level, public. response = self.testapp.post_json( '/api/responses', { 'private': False, 'user_id': teammate.uid, 'team_id': team.uid, 'parent_id': cycles[1].uid, 'module_label': 'ModuleBaz', 'body': self.default_body(), }, headers=jwt_headers(teammate), ) user_fetched = Response.get_by_id(json.loads(response.body)['uid']) self.assertFalse(user_fetched.private) # Can't create responses for other teams. self.testapp.post_json( '/api/responses', { 'type': 'Team', 'user_id': '', 'team_id': 'Team_other', 'parent_id': cycles[0].uid, 'module_label': 'ModuleFoo', }, headers=jwt_headers(other), status=403, ) # Success for own team, team-level, public. response = self.testapp.post_json( '/api/responses', { 'type': 'Team', 'user_id': '', 'team_id': team.uid, 'parent_id': 'launch-step', 'module_label': 'ModuleBar', 'body': self.default_body(), }, headers=jwt_headers(teammate), ) team_fetched = Response.get_by_id(json.loads(response.body)['uid']) self.assertFalse(team_fetched.private)