예제 #1
0
    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))
예제 #2
0
    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'],
        )
예제 #3
0
    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)
예제 #4
0
    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)
예제 #5
0
    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,
            ))
예제 #6
0
    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)
예제 #7
0
    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)
예제 #8
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')
예제 #9
0
    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)