Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/api/v3/test/issues_servicer_test.py b/api/v3/test/issues_servicer_test.py
new file mode 100644
index 0000000..7cfee41
--- /dev/null
+++ b/api/v3/test/issues_servicer_test.py
@@ -0,0 +1,890 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the issues servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import copy
+import unittest
+import mock
+
+from api.v3 import converters
+from api.v3 import issues_servicer
+from api.v3.api_proto import issues_pb2
+from api.v3.api_proto import issue_objects_pb2
+from framework import exceptions
+from framework import framework_helpers
+from framework import monorailcontext
+from framework import permissions
+from proto import tracker_pb2
+from testing import fake
+from services import service_manager
+
+from google.appengine.ext import testbed
+from google.protobuf import timestamp_pb2
+from google.protobuf import field_mask_pb2
+
+
+def _Issue(project_id, local_id):
+ issue = tracker_pb2.Issue(owner_id=0)
+ issue.project_name = 'proj-%d' % project_id
+ issue.project_id = project_id
+ issue.local_id = local_id
+ issue.issue_id = project_id * 100 + local_id
+ return issue
+
+
+CURRENT_TIME = 12346.78
+
+
+class IssuesServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ # memcache and datastore needed for generating page tokens.
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_datastore_v3_stub()
+
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ issue_star=fake.IssueStarService(),
+ project=fake.ProjectService(),
+ features=fake.FeaturesService(),
+ spam=fake.SpamService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+ self.issues_svcr = issues_servicer.IssuesServicer(
+ self.services, make_rate_limiter=False)
+ self.PAST_TIME = int(CURRENT_TIME - 1)
+
+ self.owner = self.services.user.TestAddUser('owner@example.com', 111)
+ self.user_2 = self.services.user.TestAddUser('user_2@example.com', 222)
+
+ self.project_1 = self.services.project.TestAddProject(
+ 'chicken', project_id=789)
+ self.issue_1_resource_name = 'projects/chicken/issues/1234'
+ self.issue_1 = fake.MakeTestIssue(
+ self.project_1.project_id,
+ 1234,
+ 'sum',
+ 'New',
+ self.owner.user_id,
+ labels=['find-me', 'pri-3'],
+ project_name=self.project_1.project_name)
+ self.services.issue.TestAddIssue(self.issue_1)
+
+ self.project_2 = self.services.project.TestAddProject('cow', project_id=788)
+ self.issue_2_resource_name = 'projects/cow/issues/1235'
+ self.issue_2 = fake.MakeTestIssue(
+ self.project_2.project_id,
+ 1235,
+ 'sum',
+ 'New',
+ self.user_2.user_id,
+ project_name=self.project_2.project_name)
+ self.services.issue.TestAddIssue(self.issue_2)
+ self.issue_3 = fake.MakeTestIssue(
+ self.project_2.project_id,
+ 1236,
+ 'sum',
+ 'New',
+ self.user_2.user_id,
+ labels=['find-me', 'pri-1'],
+ project_name=self.project_2.project_name)
+ self.services.issue.TestAddIssue(self.issue_3)
+
+ def CallWrapped(self, wrapped_handler, mc, *args, **kwargs):
+ self.issues_svcr.converter = converters.Converter(mc, self.services)
+ return wrapped_handler.wrapped(self.issues_svcr, mc, *args, **kwargs)
+
+ def testGetIssue(self):
+ """We can get an issue."""
+ request = issues_pb2.GetIssueRequest(name=self.issue_1_resource_name)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ actual_response = self.CallWrapped(self.issues_svcr.GetIssue, mc, request)
+ self.assertEqual(
+ actual_response, self.issues_svcr.converter.ConvertIssue(self.issue_1))
+
+ def testBatchGetIssues(self):
+ """We can batch get issues."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.BatchGetIssuesRequest(
+ names=['projects/cow/issues/1235', 'projects/cow/issues/1236'])
+ actual_response = self.CallWrapped(
+ self.issues_svcr.BatchGetIssues, mc, request)
+ self.assertEqual(
+ [issue.name for issue in actual_response.issues],
+ ['projects/cow/issues/1235', 'projects/cow/issues/1236'])
+
+ def testBatchGetIssues_Empty(self):
+ """We can return a response if the request has no names."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.BatchGetIssuesRequest(names=[])
+ actual_response = self.CallWrapped(
+ self.issues_svcr.BatchGetIssues, mc, request)
+ self.assertEqual(
+ actual_response, issues_pb2.BatchGetIssuesResponse(issues=[]))
+
+ def testBatchGetIssues_WithParent(self):
+ """We can batch get issues with a given parent."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.BatchGetIssuesRequest(
+ parent='projects/cow',
+ names=['projects/cow/issues/1235', 'projects/cow/issues/1236'])
+ actual_response = self.CallWrapped(
+ self.issues_svcr.BatchGetIssues, mc, request)
+ self.assertEqual(
+ [issue.name for issue in actual_response.issues],
+ ['projects/cow/issues/1235', 'projects/cow/issues/1236'])
+
+ def testBatchGetIssues_FromMultipleProjects(self):
+ """We can batch get issues from multiple projects."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.BatchGetIssuesRequest(
+ names=[
+ 'projects/chicken/issues/1234', 'projects/cow/issues/1235',
+ 'projects/cow/issues/1236'
+ ])
+ actual_response = self.CallWrapped(
+ self.issues_svcr.BatchGetIssues, mc, request)
+ self.assertEqual(
+ [issue.name for issue in actual_response.issues], [
+ 'projects/chicken/issues/1234', 'projects/cow/issues/1235',
+ 'projects/cow/issues/1236'
+ ])
+
+ def testBatchGetIssues_WithBadInput(self):
+ """We raise an exception with bad input to batch get issues."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.BatchGetIssuesRequest(
+ parent='projects/cow',
+ names=['projects/cow/issues/1235', 'projects/chicken/issues/1234'])
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'projects/chicken/issues/1234 is not a child issue of projects/cow.'):
+ self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
+
+ request = issues_pb2.BatchGetIssuesRequest(
+ parent='projects/sheep',
+ names=['projects/cow/issues/1235', 'projects/chicken/issues/1234'])
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'projects/cow/issues/1235 is not a child issue of projects/sheep.\n' +
+ 'projects/chicken/issues/1234 is not a child issue of projects/sheep.'):
+ self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
+
+ request = issues_pb2.BatchGetIssuesRequest(
+ parent='projects/cow',
+ names=['projects/cow/badformat/1235', 'projects/chicken/issues/1234'])
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'Invalid resource name: projects/cow/badformat/1235.'):
+ self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
+
+ def testBatchGetIssues_NonExistentIssues(self):
+ """We raise an exception with bad input to batch get issues."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.BatchGetIssuesRequest(
+ parent='projects/chicken',
+ names=['projects/chicken/issues/1', 'projects/chicken/issues/2'])
+ with self.assertRaisesRegexp(
+ exceptions.NoSuchIssueException,
+ "\['projects/chicken/issues/1', 'projects/chicken/issues/2'\] not found"
+ ):
+ self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
+
+ @mock.patch('api.v3.api_constants.MAX_BATCH_ISSUES', 2)
+ def testBatchGetIssues(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.BatchGetIssuesRequest(
+ parent='projects/cow',
+ names=[
+ 'projects/cow/issues/1235', 'projects/chicken/issues/1234',
+ 'projects/cow/issues/1233'
+ ])
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
+
+ @mock.patch('search.frontendsearchpipeline.FrontendSearchPipeline')
+ @mock.patch('api.v3.api_constants.MAX_ISSUES_PER_PAGE', 2)
+ def testSearchIssues(self, mock_pipeline):
+ """We can search for issues in some projects."""
+ request = issues_pb2.SearchIssuesRequest(
+ projects=['projects/chicken', 'projects/cow'],
+ query='label:find-me',
+ order_by='-pri',
+ page_size=3)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_2.email)
+
+ instance = mock.Mock(
+ spec=True, total_count=3, visible_results=[self.issue_1, self.issue_3])
+ mock_pipeline.return_value = instance
+ instance.SearchForIIDs = mock.Mock()
+ instance.MergeAndSortIssues = mock.Mock()
+ instance.Paginate = mock.Mock()
+
+ actual_response = self.CallWrapped(
+ self.issues_svcr.SearchIssues, mc, request)
+ # start index is 0.
+ # number of items is coerced from 3 -> 2
+ mock_pipeline.assert_called_once_with(
+ self.cnxn,
+ self.services,
+ mc.auth, [222],
+ 'label:find-me', ['chicken', 'cow'],
+ 2,
+ 0,
+ 1,
+ '',
+ '-pri',
+ mc.warnings,
+ mc.errors,
+ True,
+ mc.profiler,
+ project=None)
+ self.assertEqual(
+ [issue.name for issue in actual_response.issues],
+ ['projects/chicken/issues/1234', 'projects/cow/issues/1236'])
+
+ # Check the `next_page_token` can be used to get the next page of results.
+ request.page_token = actual_response.next_page_token
+ self.CallWrapped(self.issues_svcr.SearchIssues, mc, request)
+ # start index is now 2.
+ mock_pipeline.assert_called_with(
+ self.cnxn,
+ self.services,
+ mc.auth, [222],
+ 'label:find-me', ['chicken', 'cow'],
+ 2,
+ 2,
+ 1,
+ '',
+ '-pri',
+ mc.warnings,
+ mc.errors,
+ True,
+ mc.profiler,
+ project=None)
+
+ @mock.patch('search.frontendsearchpipeline.FrontendSearchPipeline')
+ @mock.patch('api.v3.api_constants.MAX_ISSUES_PER_PAGE', 2)
+ def testSearchIssues_PaginationErrorOrderByChanged(self, mock_pipeline):
+ """Error when changing the order_by and using the same page_otoken."""
+ request = issues_pb2.SearchIssuesRequest(
+ projects=['projects/chicken', 'projects/cow'],
+ query='label:find-me',
+ order_by='-pri',
+ page_size=3)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_2.email)
+
+ instance = mock.Mock(
+ spec=True, total_count=3, visible_results=[self.issue_1, self.issue_3])
+ mock_pipeline.return_value = instance
+ instance.SearchForIIDs = mock.Mock()
+ instance.MergeAndSortIssues = mock.Mock()
+ instance.Paginate = mock.Mock()
+
+ actual_response = self.CallWrapped(
+ self.issues_svcr.SearchIssues, mc, request)
+
+ # The request should fail if we use `next_page_token` and change parameters.
+ request.page_token = actual_response.next_page_token
+ request.order_by = 'owner'
+ with self.assertRaises(exceptions.PageTokenException):
+ self.CallWrapped(self.issues_svcr.SearchIssues, mc, request)
+
+ # Note the 'empty' case doesn't make sense for ListComments, as one is created
+ # for every issue.
+ def testListComments(self):
+ comment_2 = tracker_pb2.IssueComment(
+ id=123,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='comment 2')
+ self.services.issue.TestAddComment(comment_2, self.issue_1.local_id)
+ request = issues_pb2.ListCommentsRequest(
+ parent=self.issue_1_resource_name, page_size=1)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ actual_response = self.CallWrapped(
+ self.issues_svcr.ListComments, mc, request)
+ self.assertEqual(len(actual_response.comments), 1)
+
+ # Check the `next_page_token` can be used to get the next page of results
+ request.page_token = actual_response.next_page_token
+ next_actual_response = self.CallWrapped(
+ self.issues_svcr.ListComments, mc, request)
+ self.assertEqual(len(next_actual_response.comments), 1)
+ self.assertEqual(next_actual_response.comments[0].content, 'comment 2')
+
+ def testListComments_UnsupportedFilter(self):
+ """If anything other than approval is provided, it's an error."""
+ filter_str = 'content = "x"'
+ request = issues_pb2.ListCommentsRequest(
+ parent=self.issue_1_resource_name, page_size=1, filter=filter_str)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.ListComments, mc, request)
+
+ def testListComments_TwoApprovalsErrors(self):
+ """If anything other than a single approval is provided, it's an error."""
+ filter_str = (
+ 'approval = "projects/chicken/approvalDefs/404" OR '
+ 'approval = "projects/chicken/approvalDefs/405')
+ request = issues_pb2.ListCommentsRequest(
+ parent=self.issue_1_resource_name, page_size=1, filter=filter_str)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.ListComments, mc, request)
+
+ def testListComments_FilterTypoError(self):
+ """Even an extra space is an error."""
+ filter_str = 'approval = "projects/chicken/approvalDefs/404" '
+ request = issues_pb2.ListCommentsRequest(
+ parent=self.issue_1_resource_name, page_size=1, filter=filter_str)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.ListComments, mc, request)
+
+ def testListComments_UnknownApprovalInFilter(self):
+ """Filter with unknown approval returns no error and no comments."""
+ approval_comment = tracker_pb2.IssueComment(
+ id=123,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='comment 2 - approval 1',
+ approval_id=1)
+ self.services.issue.TestAddComment(approval_comment, self.issue_1.local_id)
+ request = issues_pb2.ListCommentsRequest(
+ parent=self.issue_1_resource_name, page_size=1,
+ filter='approval = "projects/chicken/approvalDefs/404"')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(self.issues_svcr.ListComments, mc, request)
+ self.assertEqual(len(response.comments), 0)
+
+ def testListComments_ApprovalInFilter(self):
+ approval_comment = tracker_pb2.IssueComment(
+ id=123,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='comment 2 - approval 1',
+ approval_id=1)
+ self.services.issue.TestAddComment(approval_comment, self.issue_1.local_id)
+ request = issues_pb2.ListCommentsRequest(
+ parent=self.issue_1_resource_name, page_size=1,
+ filter='approval = "projects/chicken/approvalDefs/1"')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(self.issues_svcr.ListComments, mc, request)
+ self.assertEqual(len(response.comments), 1)
+ self.assertEqual(response.comments[0].content, approval_comment.content)
+
+ def testListApprovalValues(self):
+ config = fake.MakeTestConfig(self.project_2.project_id, [], [])
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ # Make regular field def and value
+ fd_1 = fake.MakeTestFieldDef(
+ 1, self.project_2.project_id, tracker_pb2.FieldTypes.STR_TYPE,
+ field_name='field1')
+ self.services.config.TestAddFieldDef(fd_1)
+ fv_1 = fake.MakeFieldValue(
+ field_id=fd_1.field_id, str_value='value1', derived=False)
+
+ # Make testing approval def and its associated field def
+ approval_gate = fake.MakeTestFieldDef(
+ 2, self.project_2.project_id, tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ field_name='approval-gate-1')
+ self.services.config.TestAddFieldDef(approval_gate)
+ ad = fake.MakeTestApprovalDef(2, approver_ids=[self.user_2.user_id])
+ self.services.config.TestAddApprovalDef(ad, self.project_2.project_id)
+
+ # Make approval value
+ av = fake.MakeApprovalValue(2, set_on=self.PAST_TIME,
+ approver_ids=[self.user_2.user_id], setter_id=self.user_2.user_id)
+
+ # Make field def that belongs to above approval_def
+ fd_2 = fake.MakeTestFieldDef(
+ 3, self.project_2.project_id, tracker_pb2.FieldTypes.STR_TYPE,
+ field_name='field2', approval_id=2)
+ self.services.config.TestAddFieldDef(fd_2)
+ fv_2 = fake.MakeFieldValue(
+ field_id=fd_2.field_id, str_value='value2', derived=False)
+
+ issue_resource_name = 'projects/cow/issues/1237'
+ issue = fake.MakeTestIssue(
+ self.project_2.project_id,
+ 1237,
+ 'sum',
+ 'New',
+ self.user_2.user_id,
+ project_name=self.project_2.project_name,
+ field_values=[fv_1, fv_2],
+ approval_values=[av])
+ self.services.issue.TestAddIssue(issue)
+
+ request = issues_pb2.ListApprovalValuesRequest(parent=issue_resource_name)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ actual_response = self.CallWrapped(
+ self.issues_svcr.ListApprovalValues, mc, request)
+
+ self.assertEqual(len(actual_response.approval_values), 1)
+ expected_fv = issue_objects_pb2.FieldValue(
+ field='projects/cow/fieldDefs/3',
+ value='value2',
+ derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'))
+ expected = issue_objects_pb2.ApprovalValue(
+ name='projects/cow/issues/1237/approvalValues/2',
+ status=issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NOT_SET'),
+ approvers=['users/222'],
+ approval_def='projects/cow/approvalDefs/2',
+ set_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ setter='users/222',
+ field_values=[expected_fv])
+ self.assertEqual(actual_response.approval_values[0], expected)
+
+ def testListApprovalValues_Empty(self):
+ request = issues_pb2.ListApprovalValuesRequest(
+ parent=self.issue_1_resource_name)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ actual_response = self.CallWrapped(
+ self.issues_svcr.ListApprovalValues, mc, request)
+ self.assertEqual(len(actual_response.approval_values), 0)
+
+ @mock.patch(
+ 'features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testMakeIssue(self, _fake_pasicn):
+ request_issue = issue_objects_pb2.Issue(
+ summary='sum',
+ status=issue_objects_pb2.Issue.StatusValue(status='New'),
+ cc_users=[issue_objects_pb2.Issue.UserValue(user='users/222')],
+ labels=[issue_objects_pb2.Issue.LabelValue(label='foo-bar')]
+ )
+ request = issues_pb2.MakeIssueRequest(
+ parent='projects/chicken',
+ issue=request_issue,
+ description='description'
+ )
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.MakeIssue, mc, request)
+ self.assertEqual(response.summary, 'sum')
+ self.assertEqual(response.status.status, 'New')
+ self.assertEqual(response.cc_users[0].user, 'users/222')
+ self.assertEqual(response.labels[0].label, 'foo-bar')
+ self.assertEqual(response.star_count, 1)
+
+ @mock.patch(
+ 'features.send_notifications.PrepareAndSendIssueChangeNotification')
+ @mock.patch('time.time')
+ def testModifyIssues(self, fake_time, fake_notify):
+ fake_time.return_value = 12345
+
+ issue = _Issue(780, 1)
+ self.services.project.TestAddProject(
+ issue.project_name, project_id=issue.project_id,
+ owner_ids=[self.owner.user_id])
+
+ issue.labels = ['keep-me', 'remove-me']
+ self.services.issue.TestAddIssue(issue)
+ exp_issue = copy.deepcopy(issue)
+
+ self.services.issue.CreateIssueComment = mock.Mock()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+
+ request = issues_pb2.ModifyIssuesRequest(
+ deltas=[
+ issues_pb2.IssueDelta(
+ issue=issue_objects_pb2.Issue(
+ name='projects/proj-780/issues/1',
+ labels=[issue_objects_pb2.Issue.LabelValue(
+ label='add-me')]),
+ update_mask=field_mask_pb2.FieldMask(paths=['labels']),
+ labels_remove=['remove-me'])],
+ uploads=[issues_pb2.AttachmentUpload(
+ filename='mowgli.gif', content='cute dog')],
+ comment_content='Release the chicken.',
+ notify_type=issues_pb2.NotifyType.Value('NO_NOTIFICATION'))
+
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyIssues, mc, request)
+ exp_issue.labels = ['keep-me', 'add-me']
+ exp_issue.modified_timestamp = 12345
+ exp_api_issue = self.issues_svcr.converter.ConvertIssue(exp_issue)
+ self.assertEqual([iss for iss in response.issues], [exp_api_issue])
+
+ # All updated issues should have been fetched from DB, skipping cache.
+ # So we expect assume_stale=False was applied to all issues during the
+ # the fetch.
+ exp_issue.assume_stale = False
+ # These derived values get set to the following when an issue goes through
+ # the ApplyFilterRules path. (see filter_helpers._ComputeDerivedFields)
+ exp_issue.derived_owner_id = 0
+ exp_issue.derived_status = ''
+ exp_attachments = [framework_helpers.AttachmentUpload(
+ 'mowgli.gif', 'cute dog', 'image/gif')]
+ exp_amendments = [tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.LABELS, newvalue='-remove-me add-me')]
+ self.services.issue.CreateIssueComment.assert_called_once_with(
+ self.cnxn, exp_issue, mc.auth.user_id, 'Release the chicken.',
+ attachments=exp_attachments, amendments=exp_amendments, commit=False)
+ fake_notify.assert_called_once_with(
+ issue.issue_id, 'testing-app.appspot.com', self.owner.user_id,
+ comment_id=mock.ANY, old_owner_id=None, send_email=False)
+
+ def testModifyIssues_Empty(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.ModifyIssuesRequest()
+ response = self.CallWrapped(self.issues_svcr.ModifyIssues, mc, request)
+ self.assertEqual(response, issues_pb2.ModifyIssuesResponse())
+
+ @mock.patch('api.v3.api_constants.MAX_MODIFY_ISSUES', 2)
+ @mock.patch('api.v3.api_constants.MAX_MODIFY_IMPACTED_ISSUES', 4)
+ def testModifyIssues_TooMany(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.ModifyIssuesRequest(
+ deltas=[
+ issues_pb2.IssueDelta(),
+ issues_pb2.IssueDelta(),
+ issues_pb2.IssueDelta()
+ ])
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'Requesting 3 updates when the allowed maximum is 2 updates.'):
+ self.CallWrapped(self.issues_svcr.ModifyIssues, mc, request)
+
+ issue_ref_list = [issue_objects_pb2.IssueRef()]
+ request = issues_pb2.ModifyIssuesRequest(
+ deltas=[
+ issues_pb2.IssueDelta(
+ issue=issue_objects_pb2.Issue(
+ blocked_on_issue_refs=issue_ref_list),
+ blocked_on_issues_remove=issue_ref_list,
+ update_mask=field_mask_pb2.FieldMask(
+ paths=['merged_into_issue_ref'])),
+ issues_pb2.IssueDelta(
+ issue=issue_objects_pb2.Issue(
+ blocking_issue_refs=issue_ref_list),
+ blocking_issues_remove=issue_ref_list)
+ ])
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'Updates include 5 impacted issues when the allowed maximum is 4.'):
+ self.CallWrapped(self.issues_svcr.ModifyIssues, mc, request)
+
+ @mock.patch('time.time', mock.MagicMock(return_value=CURRENT_TIME))
+ @mock.patch(
+ 'features.send_notifications.PrepareAndSendApprovalChangeNotification')
+ def testModifyIssueApprovalValues(self, fake_notify):
+ self.services.issue.DeltaUpdateIssueApproval = mock.Mock()
+ config = fake.MakeTestConfig(self.project_1.project_id, [], [])
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ # Make testing approval def and its associated field def
+ field_id = 2
+ approval_field_def = fake.MakeTestFieldDef(
+ field_id,
+ self.project_1.project_id,
+ tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ field_name='approval-gate-1')
+ self.services.config.TestAddFieldDef(approval_field_def)
+ ad = fake.MakeTestApprovalDef(field_id, approver_ids=[self.owner.user_id])
+ self.services.config.TestAddApprovalDef(ad, self.project_1.project_id)
+
+ # Make approval value
+ av = fake.MakeApprovalValue(
+ field_id,
+ status=tracker_pb2.ApprovalStatus.NEEDS_REVIEW,
+ set_on=self.PAST_TIME,
+ approver_ids=[self.owner.user_id],
+ setter_id=self.user_2.user_id)
+
+ issue = fake.MakeTestIssue(
+ self.project_1.project_id,
+ 1237,
+ 'sum',
+ 'New',
+ self.owner.user_id,
+ project_name=self.project_1.project_name,
+ approval_values=[av])
+ self.services.issue.TestAddIssue(issue)
+
+ av_name = 'projects/%s/issues/%d/approvalValues/%d' % (
+ self.project_1.project_name, issue.local_id, ad.approval_id)
+ delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(
+ name=av_name,
+ status=issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NA')),
+ update_mask=field_mask_pb2.FieldMask(paths=['status']))
+
+ request = issues_pb2.ModifyIssueApprovalValuesRequest(deltas=[delta],)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyIssueApprovalValues, mc, request)
+ expected_ingested_delta = tracker_pb2.ApprovalDelta(
+ status=tracker_pb2.ApprovalStatus.NA,
+ set_on=int(CURRENT_TIME),
+ setter_id=self.owner.user_id,
+ )
+ # NOTE: Because we mock out DeltaUpdateIssueApproval, the ApprovalValues
+ # returned haven't been changed in this test. We can't test that it was
+ # changed correctly, but we can make sure it's for the right ApprovalValue.
+ self.assertEqual(len(response.approval_values), 1)
+ self.assertEqual(response.approval_values[0].name, av_name)
+ self.services.issue.DeltaUpdateIssueApproval.assert_called_once_with(
+ mc.cnxn,
+ self.owner.user_id,
+ config,
+ issue,
+ av,
+ expected_ingested_delta,
+ comment_content=u'',
+ is_description=False,
+ attachments=None,
+ kept_attachments=None)
+ fake_notify.assert_called_once_with(
+ issue.issue_id,
+ ad.approval_id,
+ 'testing-app.appspot.com',
+ mock.ANY,
+ send_email=True)
+
+ @mock.patch('api.v3.api_constants.MAX_MODIFY_APPROVAL_VALUES', 2)
+ def testModifyIssueApprovalValues_TooMany(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.ModifyIssueApprovalValuesRequest(
+ deltas=[
+ issues_pb2.ApprovalDelta(),
+ issues_pb2.ApprovalDelta(),
+ issues_pb2.ApprovalDelta()
+ ])
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.ModifyIssueApprovalValues, mc, request)
+
+ def testModifyIssueApprovalValues_Empty(self):
+ request = issues_pb2.ModifyIssueApprovalValuesRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyIssueApprovalValues, mc, request)
+ self.assertEqual(len(response.approval_values), 0)
+
+ @mock.patch(
+ 'businesslogic.work_env.WorkEnv.GetIssue',
+ return_value=tracker_pb2.Issue(
+ owner_id=0,
+ project_name='chicken',
+ project_id=789,
+ local_id=1234,
+ issue_id=80134))
+ def testModifyCommentState(self, mocked_get_issue):
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ with self.assertRaises(exceptions.NoSuchCommentException):
+ self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
+ mocked_get_issue.assert_any_call(self.issue_1.issue_id, use_cache=False)
+
+ def testModifyCommentState_Delete(self):
+ comment_1 = tracker_pb2.IssueComment(
+ id=124,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='first actual comment')
+ self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
+
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyCommentState, mc, request)
+ self.assertEqual(response.comment.state, state)
+ self.assertEqual(response.comment.content, 'first actual comment')
+
+ # Test noop
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyCommentState, mc, request)
+ self.assertEqual(response.comment.state, state)
+
+ # Test undelete
+ state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyCommentState, mc, request)
+ self.assertEqual(response.comment.state, state)
+
+ @mock.patch(
+ 'framework.permissions.UpdateIssuePermissions',
+ return_value=permissions.ADMIN_PERMISSIONSET)
+ def testModifyCommentState_Spam(self, _mocked):
+ comment_1 = tracker_pb2.IssueComment(
+ id=124,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='first actual comment')
+ self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
+
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('SPAM')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyCommentState, mc, request)
+ self.assertEqual(response.comment.state, state)
+
+ # Test noop
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyCommentState, mc, request)
+ self.assertEqual(response.comment.state, state)
+
+ # Test unflag as spam
+ state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyCommentState, mc, request)
+ self.assertEqual(response.comment.state, state)
+
+ def testModifyCommentState_Active(self):
+ comment_1 = tracker_pb2.IssueComment(
+ id=124,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='first actual comment')
+ self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
+
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyCommentState, mc, request)
+ self.assertEqual(response.comment.state, state)
+
+ def testModifyCommentState_Spam_ActionNotSupported(self):
+ # Cannot transition from deleted to spam
+ comment_1 = tracker_pb2.IssueComment(
+ id=124,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='first actual comment',
+ deleted_by=self.owner.user_id)
+ self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
+
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('SPAM')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ with self.assertRaises(exceptions.ActionNotSupported):
+ self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
+
+ def testModifyCommentState_Delete_ActionNotSupported(self):
+ # Cannot transition from spam to deleted
+ comment_1 = tracker_pb2.IssueComment(
+ id=124,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='first actual comment',
+ is_spam=True)
+ self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
+
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ with self.assertRaises(exceptions.ActionNotSupported):
+ self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
+
+ def testModifyCommentState_NoSuchComment(self):
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ with self.assertRaises(exceptions.NoSuchCommentException):
+ self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
+
+ def testModifyCommentState_Delete_PermissionException(self):
+ comment_1 = tracker_pb2.IssueComment(
+ id=124,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='first actual comment')
+ self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
+
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_2.email)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
+
+ @mock.patch(
+ 'framework.permissions.UpdateIssuePermissions',
+ return_value=permissions.READ_ONLY_PERMISSIONSET)
+ def testModifyCommentState_Spam_PermissionException(self, _mocked):
+ comment_1 = tracker_pb2.IssueComment(
+ id=124,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='first actual comment')
+ self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
+
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('SPAM')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_2.email)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)