456 строки
18 KiB
Python
456 строки
18 KiB
Python
# Copyright 2020 Google Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License")
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import datetime
|
|
import testing_config # Must be imported before the module under test.
|
|
|
|
import flask
|
|
from unittest import mock
|
|
import werkzeug.exceptions # Flask HTTP stuff.
|
|
from google.cloud import ndb # type: ignore
|
|
|
|
from api import approvals_api
|
|
from internals import core_models
|
|
from internals.review_models import Approval, ApprovalConfig, Gate, Vote
|
|
|
|
test_app = flask.Flask(__name__)
|
|
|
|
NOW = datetime.datetime.now()
|
|
|
|
|
|
class ApprovalsAPITest(testing_config.CustomTestCase):
|
|
|
|
def setUp(self):
|
|
self.feature_1 = core_models.FeatureEntry(
|
|
name='feature one', summary='sum', category=1)
|
|
self.feature_1.put()
|
|
self.feature_id = self.feature_1.key.integer_id()
|
|
|
|
self.gate_1 = Gate(id=1, feature_id=self.feature_id, stage_id=1,
|
|
gate_type=1, state=Vote.NA)
|
|
self.gate_1.put()
|
|
self.gate_2 = Gate(id=2, feature_id=self.feature_id, stage_id=2,
|
|
gate_type=2, state=Vote.NA)
|
|
self.gate_2.put()
|
|
|
|
self.handler = approvals_api.ApprovalsAPI()
|
|
self.request_path = '/api/v0/features/%d/approvals' % self.feature_id
|
|
|
|
# These are not in the datastore unless a specific test calls put().
|
|
self.appr_1_1 = Approval(
|
|
feature_id=self.feature_id, field_id=1,
|
|
set_by='owner1@example.com', set_on=NOW,
|
|
state=Approval.APPROVED)
|
|
self.appr_1_2 = Approval(
|
|
feature_id=self.feature_id, field_id=2,
|
|
set_by='owner2@example.com', set_on=NOW,
|
|
state=Approval.NEEDS_WORK)
|
|
|
|
# Vote entity equivalents.
|
|
self.vote_1_1 = Vote(feature_id=self.feature_id, gate_id=10,
|
|
gate_type=1, set_on=NOW,
|
|
set_by='owner1@example.com', state=Vote.APPROVED)
|
|
self.vote_1_2 = Vote(feature_id=self.feature_id, gate_id=11, gate_type=2,
|
|
set_by='owner2@example.com', set_on=NOW, state=Vote.NEEDS_WORK)
|
|
|
|
self.expected1 = {
|
|
'feature_id': self.feature_id,
|
|
'field_id': 1,
|
|
'set_by': 'owner1@example.com',
|
|
'set_on': str(NOW),
|
|
'state': Approval.APPROVED,
|
|
}
|
|
self.expected2 = {
|
|
'feature_id': self.feature_id,
|
|
'field_id': 2,
|
|
'set_by': 'owner2@example.com',
|
|
'set_on': str(NOW),
|
|
'state': Approval.NEEDS_WORK,
|
|
}
|
|
|
|
self.vote_expected1 = {
|
|
'feature_id': self.feature_id,
|
|
'gate_id': 10,
|
|
'gate_type': 1,
|
|
'set_by': 'owner1@example.com',
|
|
'set_on': str(NOW),
|
|
'state': Vote.APPROVED,
|
|
}
|
|
self.vote_expected2 = {
|
|
'feature_id': self.feature_id,
|
|
'gate_id': 11,
|
|
'gate_type': 2,
|
|
'set_by': 'owner2@example.com',
|
|
'set_on': str(NOW),
|
|
'state': Vote.NEEDS_WORK,
|
|
}
|
|
|
|
def tearDown(self):
|
|
self.feature_1.key.delete()
|
|
kinds: list[ndb.Model] = [Approval, Gate, Vote]
|
|
for kind in kinds:
|
|
for entity in kind.query():
|
|
entity.key.delete()
|
|
|
|
def test_get__all_empty(self):
|
|
"""We can get all approvals for a given feature, even if there none."""
|
|
testing_config.sign_out()
|
|
with test_app.test_request_context(self.request_path):
|
|
actual_response = self.handler.do_get(feature_id=self.feature_id)
|
|
self.assertEqual({"approvals": []}, actual_response)
|
|
|
|
def test_get__all_some(self):
|
|
"""We can get all approvals for a given feature."""
|
|
testing_config.sign_out()
|
|
self.vote_1_1.put()
|
|
self.vote_1_2.put()
|
|
|
|
with test_app.test_request_context(self.request_path):
|
|
actual_response = self.handler.do_get(feature_id=self.feature_id)
|
|
|
|
self.assertEqual(
|
|
{"approvals": [self.vote_expected1, self.vote_expected2]},
|
|
actual_response)
|
|
|
|
def test_get__field_empty(self):
|
|
"""We can get approvals for given feature and field, even if there none."""
|
|
testing_config.sign_out()
|
|
with test_app.test_request_context(self.request_path + '/1'):
|
|
actual_response = self.handler.do_get(
|
|
feature_id=self.feature_id, gate_type=1)
|
|
self.assertEqual({"approvals": []}, actual_response)
|
|
|
|
def test_get__field_some(self):
|
|
"""We can get approvals for a given feature and gate_type."""
|
|
testing_config.sign_out()
|
|
self.vote_1_1.put()
|
|
self.vote_1_2.put()
|
|
|
|
with test_app.test_request_context(self.request_path + '/1'):
|
|
actual_response = self.handler.do_get(
|
|
feature_id=self.feature_id, gate_type=1)
|
|
|
|
self.assertEqual({"approvals": [self.vote_expected1]}, actual_response)
|
|
|
|
def test_post__bad_feature_id(self):
|
|
"""Handler rejects requests that don't specify a feature ID correctly."""
|
|
params = {}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
with self.assertRaises(werkzeug.exceptions.BadRequest):
|
|
self.handler.do_post()
|
|
|
|
params = {'featureId': 'not an int'}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
with self.assertRaises(werkzeug.exceptions.BadRequest):
|
|
self.handler.do_post()
|
|
|
|
def test_post__bad_field_id(self):
|
|
"""Handler rejects requests that don't specify a field ID correctly."""
|
|
params = {'featureId': self.feature_id}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
with self.assertRaises(werkzeug.exceptions.BadRequest):
|
|
self.handler.do_post()
|
|
|
|
params = {'featureId': self.feature_id, 'fieldId': 'not an int'}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
with self.assertRaises(werkzeug.exceptions.BadRequest):
|
|
self.handler.do_post()
|
|
|
|
params = {'featureId': self.feature_id, 'fieldId': 999}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
with self.assertRaises(werkzeug.exceptions.BadRequest):
|
|
self.handler.do_post()
|
|
|
|
def test_post__bad_state(self):
|
|
"""Handler rejects requests that don't specify a state correctly."""
|
|
params = {'featureId': self.feature_id, 'gateType': 1}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
with self.assertRaises(werkzeug.exceptions.BadRequest):
|
|
self.handler.do_post()
|
|
|
|
params = {'featureId': self.feature_id, 'gateType': 1,
|
|
'state': 'not an int'}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
with self.assertRaises(werkzeug.exceptions.BadRequest):
|
|
self.handler.do_post()
|
|
|
|
params = {'featureId': self.feature_id, 'gateType': 1,
|
|
'state': 999}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
with self.assertRaises(werkzeug.exceptions.BadRequest):
|
|
self.handler.do_post()
|
|
|
|
def test_post__feature_not_found(self):
|
|
"""Handler rejects requests that don't match an existing feature."""
|
|
params = {'featureId': 12345, 'gateType': 1,
|
|
'state': Approval.NEEDS_WORK }
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
with self.assertRaises(werkzeug.exceptions.NotFound):
|
|
self.handler.do_post()
|
|
|
|
@mock.patch('internals.approval_defs.get_approvers')
|
|
def test_post__forbidden(self, mock_get_approvers):
|
|
"""Handler rejects requests from anon users and non-approvers."""
|
|
mock_get_approvers.return_value = ['owner1@example.com']
|
|
params = {'featureId': self.feature_id, 'gateType': 1,
|
|
'state': Approval.NEEDS_WORK}
|
|
|
|
testing_config.sign_out()
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
with self.assertRaises(werkzeug.exceptions.Forbidden):
|
|
self.handler.do_post()
|
|
|
|
testing_config.sign_in('user7@example.com', 123567890)
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
with self.assertRaises(werkzeug.exceptions.Forbidden):
|
|
self.handler.do_post()
|
|
|
|
testing_config.sign_in('user@google.com', 123567890)
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
with self.assertRaises(werkzeug.exceptions.Forbidden):
|
|
self.handler.do_post()
|
|
|
|
@mock.patch('internals.approval_defs.get_approvers')
|
|
def test_post__add_or_update(self, mock_get_approvers):
|
|
"""Handler adds approval when one did not exist before."""
|
|
mock_get_approvers.return_value = ['owner1@example.com']
|
|
testing_config.sign_in('owner1@example.com', 123567890)
|
|
params = {'featureId': self.feature_id, 'gateType': 1,
|
|
'state': Vote.NEEDS_WORK}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
actual = self.handler.do_post(feature_id=self.feature_id)
|
|
|
|
self.assertEqual(actual, {'message': 'Done'})
|
|
updated_approvals = Vote.get_votes(feature_id=self.feature_id)
|
|
self.assertEqual(1, len(updated_approvals))
|
|
vote = updated_approvals[0]
|
|
self.assertEqual(vote.feature_id, self.feature_id)
|
|
self.assertEqual(vote.gate_id, 1)
|
|
self.assertEqual(vote.set_by, 'owner1@example.com')
|
|
self.assertEqual(vote.state, Vote.NEEDS_WORK)
|
|
|
|
|
|
class ApprovalConfigsAPITest(testing_config.CustomTestCase):
|
|
|
|
def setUp(self):
|
|
self.feature_1 = core_models.FeatureEntry(
|
|
name='feature one', summary='sum', category=1)
|
|
self.feature_1.put()
|
|
self.feature_1_id = self.feature_1.key.integer_id()
|
|
self.config_1 = ApprovalConfig(
|
|
feature_id=self.feature_1_id, field_id=1,
|
|
owners=['one_a@example.com', 'one_b@example.com'])
|
|
self.config_1.put()
|
|
|
|
self.feature_2 = core_models.FeatureEntry(
|
|
name='feature two', summary='sum', category=1)
|
|
self.feature_2.put()
|
|
self.feature_2_id = self.feature_2.key.integer_id()
|
|
self.config_2 = ApprovalConfig(
|
|
feature_id=self.feature_2_id, field_id=2,
|
|
owners=['two_a@example.com', 'two_b@example.com'])
|
|
self.config_2.put()
|
|
|
|
self.feature_3 = core_models.FeatureEntry(
|
|
name='feature three', summary='sum', category=1)
|
|
self.feature_3.put()
|
|
self.feature_3_id = self.feature_3.key.integer_id()
|
|
# Feature 3 has no configs
|
|
|
|
self.handler = approvals_api.ApprovalConfigsAPI()
|
|
self.request_path = '/api/v0/features/%d/configs' % self.feature_1_id
|
|
|
|
def tearDown(self):
|
|
self.feature_1.key.delete()
|
|
self.feature_2.key.delete()
|
|
self.feature_3.key.delete()
|
|
for config in ApprovalConfig.query():
|
|
config.key.delete()
|
|
|
|
@mock.patch('internals.approval_defs.get_approvers')
|
|
def test_do_get__found(self, mock_get_approvers):
|
|
"""Anon or any user can get some configs."""
|
|
mock_get_approvers.return_value = ['owner@example.com']
|
|
testing_config.sign_in('other@example.com', 123567890)
|
|
with test_app.test_request_context(self.request_path):
|
|
actual = self.handler.do_get(feature_id=self.feature_1_id)
|
|
self.assertEqual(
|
|
{'configs': [{
|
|
'feature_id': self.feature_1_id,
|
|
'field_id': 1,
|
|
'owners': ['one_a@example.com', 'one_b@example.com'],
|
|
'additional_review': False,
|
|
'next_action': None,
|
|
}],
|
|
'possible_owners': {
|
|
1: ['owner@example.com'],
|
|
2: ['owner@example.com'],
|
|
3: ['owner@example.com'],
|
|
4: ['owner@example.com'],
|
|
},
|
|
},
|
|
actual)
|
|
|
|
testing_config.sign_out()
|
|
with test_app.test_request_context(self.request_path):
|
|
actual = self.handler.do_get(feature_id=self.feature_2_id)
|
|
self.assertEqual(
|
|
{'configs': [{
|
|
'feature_id': self.feature_2_id,
|
|
'field_id': 2,
|
|
'owners': ['two_a@example.com', 'two_b@example.com'],
|
|
'additional_review': False,
|
|
'next_action': None,
|
|
}],
|
|
'possible_owners': {
|
|
1: ['owner@example.com'],
|
|
2: ['owner@example.com'],
|
|
3: ['owner@example.com'],
|
|
4: ['owner@example.com'],
|
|
},
|
|
},
|
|
actual)
|
|
|
|
@mock.patch('internals.approval_defs.get_approvers')
|
|
def test_do_get__no_configs(self, mock_get_approvers):
|
|
"""If there are no configs, we return an empty list."""
|
|
mock_get_approvers.return_value = ['owner@example.com']
|
|
with test_app.test_request_context(self.request_path):
|
|
actual = self.handler.do_get(feature_id=self.feature_3_id)
|
|
|
|
self.assertEqual(
|
|
{'configs': [],
|
|
'possible_owners': {
|
|
1: ['owner@example.com'],
|
|
2: ['owner@example.com'],
|
|
3: ['owner@example.com'],
|
|
4: ['owner@example.com'],
|
|
},
|
|
},
|
|
actual)
|
|
|
|
@mock.patch('internals.approval_defs.get_approvers')
|
|
def test_do_post__add_a_config(self, mock_get_approvers):
|
|
"""If there are already existing configs, we can add new one."""
|
|
mock_get_approvers.return_value = ['owner1@example.com']
|
|
testing_config.sign_in('owner1@example.com', 123567890)
|
|
|
|
params = {'fieldId': 3,
|
|
'owners': ' one@example.com, two@example.com, ',
|
|
'nextAction': '2021-11-30',
|
|
'additionalReview': False}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
actual = self.handler.do_post(feature_id=self.feature_1_id)
|
|
|
|
self.assertEqual({'message': 'Done'}, actual)
|
|
revised_configs = ApprovalConfig.query(
|
|
ApprovalConfig.feature_id == self.feature_1_id).order(
|
|
ApprovalConfig.field_id).fetch(None)
|
|
self.assertEqual(2, len(revised_configs))
|
|
revised_config_3 = revised_configs[1]
|
|
self.assertEqual(3, revised_config_3.field_id)
|
|
self.assertEqual(['one@example.com', 'two@example.com'],
|
|
revised_config_3.owners)
|
|
|
|
@mock.patch('internals.approval_defs.get_approvers')
|
|
def test_do_post__update(self, mock_get_approvers):
|
|
"""We can update an existing config."""
|
|
mock_get_approvers.return_value = ['owner1@example.com']
|
|
testing_config.sign_in('owner1@example.com', 123567890)
|
|
|
|
params = {'fieldId': 1,
|
|
'owners': 'one_a@example.com, one_b@example.com',
|
|
'nextAction': '2021-11-30',
|
|
'additionalReview': False}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
actual = self.handler.do_post(feature_id=self.feature_1_id)
|
|
|
|
self.assertEqual({'message': 'Done'}, actual)
|
|
revised_config = ApprovalConfig.query(
|
|
ApprovalConfig.feature_id == self.feature_1_id).fetch(None)[0]
|
|
self.assertEqual(self.feature_1_id, revised_config.feature_id)
|
|
self.assertEqual(1, revised_config.field_id)
|
|
self.assertEqual(datetime.date.fromisoformat('2021-11-30'),
|
|
revised_config.next_action)
|
|
self.assertEqual(['one_a@example.com', 'one_b@example.com'],
|
|
revised_config.owners)
|
|
self.assertEqual(False, revised_config.additional_review)
|
|
|
|
@mock.patch('internals.approval_defs.get_approvers')
|
|
def test_do_post__clear(self, mock_get_approvers):
|
|
"""We can update an existing config to clear values."""
|
|
mock_get_approvers.return_value = ['owner1@example.com']
|
|
testing_config.sign_in('owner1@example.com', 123567890)
|
|
|
|
params = {'fieldId': 1,
|
|
'owners': '',
|
|
'nextAction': '',
|
|
'additionalReview': False}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
actual = self.handler.do_post(feature_id=self.feature_1_id)
|
|
|
|
self.assertEqual({'message': 'Done'}, actual)
|
|
revised_config = ApprovalConfig.query(
|
|
ApprovalConfig.feature_id == self.feature_1_id).fetch(None)[0]
|
|
self.assertEqual(self.feature_1_id, revised_config.feature_id)
|
|
self.assertEqual(1, revised_config.field_id)
|
|
self.assertEqual(None, revised_config.next_action)
|
|
self.assertEqual([], revised_config.owners)
|
|
self.assertEqual(False, revised_config.additional_review)
|
|
|
|
@mock.patch('internals.approval_defs.get_approvers')
|
|
def test_do_post__no_configs(self, mock_get_approvers):
|
|
"""If there are no existing configs, we create one."""
|
|
mock_get_approvers.return_value = ['owner1@example.com']
|
|
testing_config.sign_in('owner1@example.com', 123567890)
|
|
|
|
params = {'fieldId': 3,
|
|
'owners': '',
|
|
'nextAction': '2021-11-30',
|
|
'additionalReview': False}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
actual = self.handler.do_post(feature_id=self.feature_3_id)
|
|
|
|
self.assertEqual({'message': 'Done'}, actual)
|
|
new_config = ApprovalConfig.query(
|
|
ApprovalConfig.feature_id == self.feature_3_id).fetch(None)[0]
|
|
self.assertEqual(self.feature_3_id, new_config.feature_id)
|
|
self.assertEqual(3, new_config.field_id)
|
|
self.assertEqual(datetime.date.fromisoformat('2021-11-30'),
|
|
new_config.next_action)
|
|
self.assertEqual([], new_config.owners)
|
|
self.assertEqual(False, new_config.additional_review)
|
|
|
|
@mock.patch('internals.approval_defs.get_approvers')
|
|
def test_do_post__bad_date(self, mock_get_approvers):
|
|
"""We reject bad date formats and values."""
|
|
mock_get_approvers.return_value = ['owner1@example.com']
|
|
testing_config.sign_in('owner1@example.com', 123567890)
|
|
|
|
params = {'fieldId': 3,
|
|
'owners': '',
|
|
'nextAction': '11/30/21',
|
|
'additionalReview': False}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
with self.assertRaises(werkzeug.exceptions.BadRequest):
|
|
self.handler.do_post(feature_id=self.feature_3_id)
|
|
|
|
params = {'fieldId': 3,
|
|
'owners': '',
|
|
'nextAction': '2021-11-35',
|
|
'additionalReview': False}
|
|
with test_app.test_request_context(self.request_path, json=params):
|
|
with self.assertRaises(werkzeug.exceptions.BadRequest):
|
|
self.handler.do_post(feature_id=self.feature_3_id)
|