Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/api/test/__init__.py b/api/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/api/test/__init__.py
diff --git a/api/test/converters_test.py b/api/test/converters_test.py
new file mode 100644
index 0000000..e193423
--- /dev/null
+++ b/api/test/converters_test.py
@@ -0,0 +1,2222 @@
+# Copyright 2018 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 converting internal protorpc to external protoc."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from mock import Mock, patch
+import unittest
+
+from google.protobuf import wrappers_pb2
+
+import settings
+from api import converters
+from api.api_proto import common_pb2
+from api.api_proto import features_objects_pb2
+from api.api_proto import issue_objects_pb2
+from api.api_proto import project_objects_pb2
+from api.api_proto import user_objects_pb2
+from framework import exceptions
+from framework import permissions
+from proto import tracker_pb2
+from proto import user_pb2
+from testing import fake
+from testing import testing_helpers
+from tracker import tracker_bizobj
+from services import features_svc
+from services import service_manager
+
+
+class ConverterFunctionsTest(unittest.TestCase):
+
+ NOW = 1234567890
+
+ def setUp(self):
+ self.users_by_id = {
+ 111: testing_helpers.Blank(
+ display_name='one@example.com', email='one@example.com',
+ banned=False),
+ 222: testing_helpers.Blank(
+ display_name='two@example.com', email='two@example.com',
+ banned=False),
+ 333: testing_helpers.Blank(
+ display_name='ban...@example.com', email='banned@example.com',
+ banned=True),
+ }
+
+ self.services = service_manager.Services(
+ issue=fake.IssueService(),
+ project=fake.ProjectService(),
+ user=fake.UserService(),
+ features=fake.FeaturesService())
+ self.cnxn = fake.MonorailConnection()
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=789)
+ self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+
+ self.fd_1 = tracker_pb2.FieldDef(
+ field_name='FirstField', field_id=1,
+ field_type=tracker_pb2.FieldTypes.STR_TYPE,
+ applicable_type='')
+ self.fd_2 = tracker_pb2.FieldDef(
+ field_name='SecField', field_id=2,
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ applicable_type='')
+ self.fd_3 = tracker_pb2.FieldDef(
+ field_name='LegalApproval', field_id=3,
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ applicable_type='')
+ self.fd_4 = tracker_pb2.FieldDef(
+ field_name='UserField', field_id=4,
+ field_type=tracker_pb2.FieldTypes.USER_TYPE,
+ applicable_type='')
+ self.fd_5 = tracker_pb2.FieldDef(
+ field_name='Pre', field_id=5,
+ field_type=tracker_pb2.FieldTypes.ENUM_TYPE,
+ applicable_type='')
+ self.fd_6 = tracker_pb2.FieldDef(
+ field_name='PhaseField', field_id=6,
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ applicable_type='', is_phase_field=True)
+ self.fd_7 = tracker_pb2.FieldDef(
+ field_name='ApprovalEnum', field_id=7,
+ field_type=tracker_pb2.FieldTypes.ENUM_TYPE,
+ applicable_type='', approval_id=self.fd_3.field_id)
+
+ self.user_1 = self.services.user.TestAddUser('one@example.com', 111)
+ self.user_2 = self.services.user.TestAddUser('two@example.com', 222)
+ self.user_3 = self.services.user.TestAddUser('banned@example.com', 333)
+ self.issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj')
+ self.issue_2 = fake.MakeTestIssue(
+ 789, 2, 'sum', 'New', 111, project_name='proj')
+ self.services.issue.TestAddIssue(self.issue_1)
+ self.services.issue.TestAddIssue(self.issue_2)
+
+ def testConvertApprovalValues_Empty(self):
+ """We handle the case where an issue has no approval values."""
+ actual = converters.ConvertApprovalValues([], [], {}, self.config)
+ self.assertEqual([], actual)
+
+ def testConvertApprovalValues_Normal(self):
+ """We can convert a list of approval values."""
+ now = 1234567890
+ self.config.field_defs.append(tracker_pb2.FieldDef(
+ field_id=1, project_id=789, field_name='EstDays',
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ applicable_type=''))
+ self.config.field_defs.append(tracker_pb2.FieldDef(
+ field_id=11, project_id=789, field_name='Accessibility',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ applicable_type='Launch'))
+ self.config.approval_defs.append(tracker_pb2.ApprovalDef(
+ approval_id=11, approver_ids=[111], survey='survey 1'))
+ self.config.approval_defs.append(tracker_pb2.ApprovalDef(
+ approval_id=12, approver_ids=[111], survey='survey 2'))
+ av_11 = tracker_pb2.ApprovalValue(
+ approval_id=11, status=tracker_pb2.ApprovalStatus.NEED_INFO,
+ setter_id=111, set_on=now, approver_ids=[111, 222],
+ phase_id=21)
+ # Note: no approval def, no phase, so it won't be returned.
+ # TODO(ehmaldonado): Figure out support for "foreign" fields.
+ av_12 = tracker_pb2.ApprovalValue(
+ approval_id=12, status=tracker_pb2.ApprovalStatus.NOT_SET,
+ setter_id=111, set_on=now, approver_ids=[111])
+ phase_21 = tracker_pb2.Phase(phase_id=21, name='Stable', rank=1)
+ actual = converters.ConvertApprovalValues(
+ [av_11, av_12], [phase_21], self.users_by_id, self.config)
+
+ expected_av_1 = issue_objects_pb2.Approval(
+ field_ref=common_pb2.FieldRef(
+ field_id=11,
+ field_name='Accessibility',
+ type=common_pb2.APPROVAL_TYPE),
+ approver_refs=[
+ common_pb2.UserRef(user_id=111, display_name='one@example.com'),
+ common_pb2.UserRef(user_id=222, display_name='two@example.com'),
+ ],
+ status=issue_objects_pb2.NEED_INFO,
+ set_on=now,
+ setter_ref=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Stable'))
+
+ self.assertEqual([expected_av_1], actual)
+
+ def testConvertApproval(self):
+ """We can convert ApprovalValues to protoc Approvals."""
+ approval_value = tracker_pb2.ApprovalValue(
+ approval_id=3,
+ status=tracker_pb2.ApprovalStatus.NEED_INFO,
+ setter_id=222,
+ set_on=2345,
+ approver_ids=[111],
+ phase_id=1
+ )
+
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_3]
+
+ phase = tracker_pb2.Phase(phase_id=1, name='Canary')
+
+ actual = converters.ConvertApproval(
+ approval_value, self.users_by_id, self.config, phase=phase)
+ expected = issue_objects_pb2.Approval(
+ field_ref=common_pb2.FieldRef(
+ field_id=3,
+ field_name='LegalApproval',
+ type=common_pb2.APPROVAL_TYPE),
+ approver_refs=[common_pb2.UserRef(
+ user_id=111, display_name='one@example.com', is_derived=False)
+ ],
+ status=5,
+ set_on=2345,
+ setter_ref=common_pb2.UserRef(
+ user_id=222, display_name='two@example.com', is_derived=False
+ ),
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Canary')
+ )
+
+ self.assertEqual(expected, actual)
+
+ def testConvertApproval_NonExistentApproval(self):
+ approval_value = tracker_pb2.ApprovalValue(
+ approval_id=3,
+ status=tracker_pb2.ApprovalStatus.NEED_INFO,
+ setter_id=222,
+ set_on=2345,
+ approver_ids=[111],
+ phase_id=1
+ )
+ phase = tracker_pb2.Phase(phase_id=1, name='Canary')
+ self.assertIsNone(converters.ConvertApproval(
+ approval_value, self.users_by_id, self.config, phase=phase))
+
+
+ def testConvertApprovalStatus(self):
+ """We can convert a protorpc ApprovalStatus to a protoc ApprovalStatus."""
+ actual = converters.ConvertApprovalStatus(
+ tracker_pb2.ApprovalStatus.REVIEW_REQUESTED)
+ self.assertEqual(actual, issue_objects_pb2.REVIEW_REQUESTED)
+
+ actual = converters.ConvertApprovalStatus(
+ tracker_pb2.ApprovalStatus.NOT_SET)
+ self.assertEqual(actual, issue_objects_pb2.NOT_SET)
+
+ def testConvertUserRef(self):
+ """We can convert user IDs to a UserRef."""
+ # No specified user
+ actual = converters.ConvertUserRef(None, None, self.users_by_id)
+ expected = None
+ self.assertEqual(expected, actual)
+
+ # Explicitly specified user
+ actual = converters.ConvertUserRef(111, None, self.users_by_id)
+ expected = common_pb2.UserRef(
+ user_id=111, is_derived=False, display_name='one@example.com')
+ self.assertEqual(expected, actual)
+
+ # Derived user
+ actual = converters.ConvertUserRef(None, 111, self.users_by_id)
+ expected = common_pb2.UserRef(
+ user_id=111, is_derived=True, display_name='one@example.com')
+ self.assertEqual(expected, actual)
+
+ def testConvertUserRefs(self):
+ """We can convert lists of user_ids into UserRefs."""
+ # No specified users
+ actual = converters.ConvertUserRefs(
+ [], [], self.users_by_id, False)
+ expected = []
+ self.assertEqual(expected, actual)
+
+ # A mix of explicit and derived users
+ actual = converters.ConvertUserRefs(
+ [111], [222], self.users_by_id, False)
+ expected = [
+ common_pb2.UserRef(
+ user_id=111, is_derived=False, display_name='one@example.com'),
+ common_pb2.UserRef(
+ user_id=222, is_derived=True, display_name='two@example.com'),
+ ]
+ self.assertEqual(expected, actual)
+
+ # Use display name
+ actual = converters.ConvertUserRefs([333], [], self.users_by_id, False)
+ self.assertEqual(
+ [common_pb2.UserRef(
+ user_id=333, is_derived=False, display_name='ban...@example.com')],
+ actual)
+
+ # Use email
+ actual = converters.ConvertUserRefs([333], [], self.users_by_id, True)
+ self.assertEqual(
+ [common_pb2.UserRef(
+ user_id=333, is_derived=False, display_name='banned@example.com')],
+ actual)
+
+ @patch('time.time')
+ def testConvertUsers(self, mock_time):
+ """We can convert lists of protorpc Users to protoc Users."""
+ mock_time.return_value = self.NOW
+ user1 = user_pb2.User(
+ user_id=1, email='user1@example.com', last_visit_timestamp=self.NOW)
+ user2 = user_pb2.User(
+ user_id=2, email='user2@example.com', is_site_admin=True,
+ last_visit_timestamp=self.NOW)
+ user3 = user_pb2.User(
+ user_id=3, email='user3@example.com',
+ linked_child_ids=[4])
+ user4 = user_pb2.User(
+ user_id=4, email='user4@example.com', last_visit_timestamp=1,
+ linked_parent_id=3)
+ users_by_id = {
+ 3: testing_helpers.Blank(
+ display_name='user3@example.com', email='user3@example.com',
+ banned=False),
+ 4: testing_helpers.Blank(
+ display_name='user4@example.com', email='user4@example.com',
+ banned=False),
+ }
+
+ actual = converters.ConvertUsers(
+ [user1, user2, user3, user4], users_by_id)
+ self.assertItemsEqual(
+ actual,
+ [user_objects_pb2.User(
+ user_id=1,
+ display_name='user1@example.com'),
+ user_objects_pb2.User(
+ user_id=2,
+ display_name='user2@example.com',
+ is_site_admin=True),
+ user_objects_pb2.User(
+ user_id=3,
+ display_name='user3@example.com',
+ availability='User never visited',
+ linked_child_refs=[common_pb2.UserRef(
+ user_id=4, display_name='user4@example.com')]),
+ user_objects_pb2.User(
+ user_id=4,
+ display_name='user4@example.com',
+ availability='Last visit > 30 days ago',
+ linked_parent_ref=common_pb2.UserRef(
+ user_id=3, display_name='user3@example.com')),
+ ])
+
+ def testConvetPrefValues(self):
+ """We can convert a list of UserPrefValues from protorpc to protoc."""
+ self.assertEqual(
+ [],
+ converters.ConvertPrefValues([]))
+
+ userprefvalues = [
+ user_pb2.UserPrefValue(name='foo_1', value='bar_1'),
+ user_pb2.UserPrefValue(name='foo_2', value='bar_2')]
+ actual = converters.ConvertPrefValues(userprefvalues)
+ expected = [
+ user_objects_pb2.UserPrefValue(name='foo_1', value='bar_1'),
+ user_objects_pb2.UserPrefValue(name='foo_2', value='bar_2')]
+ self.assertEqual(expected, actual)
+
+ def testConvertLabels(self):
+ """We can convert labels."""
+ # No labels specified
+ actual = converters.ConvertLabels([], [])
+ self.assertEqual([], actual)
+
+ # A mix of explicit and derived labels
+ actual = converters.ConvertLabels(
+ ['Milestone-66'], ['Restrict-View-CoreTeam'])
+ expected = [
+ common_pb2.LabelRef(label='Milestone-66', is_derived=False),
+ common_pb2.LabelRef(label='Restrict-View-CoreTeam', is_derived=True),
+ ]
+ self.assertEqual(expected, actual)
+
+ def testConvertComponentRef(self):
+ """We can convert a component ref."""
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI'),
+ tracker_pb2.ComponentDef(component_id=2, path='DB')]
+
+ self.assertEqual(
+ common_pb2.ComponentRef(
+ path='UI',
+ is_derived=False),
+ converters.ConvertComponentRef(1, self.config))
+
+ self.assertEqual(
+ common_pb2.ComponentRef(
+ path='DB',
+ is_derived=True),
+ converters.ConvertComponentRef(2, self.config, True))
+
+ self.assertIsNone(
+ converters.ConvertComponentRef(3, self.config, True))
+
+ def testConvertComponents(self):
+ """We can convert a list of components."""
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI'),
+ tracker_pb2.ComponentDef(component_id=2, path='DB'),
+ ]
+
+ # No components specified
+ actual = converters.ConvertComponents([], [], self.config)
+ self.assertEqual([], actual)
+
+ # A mix of explicit, derived, and non-existing components
+ actual = converters.ConvertComponents([1, 4], [2, 3], self.config)
+ expected = [
+ common_pb2.ComponentRef(path='UI', is_derived=False),
+ common_pb2.ComponentRef(path='DB', is_derived=True),
+ ]
+ self.assertEqual(expected, actual)
+
+ def testConvertIssueRef(self):
+ """We can convert a pair (project_name, local_id) to an IssueRef."""
+ actual = converters.ConvertIssueRef(('proj', 1))
+ self.assertEqual(
+ common_pb2.IssueRef(project_name='proj', local_id=1),
+ actual)
+
+ def testConvertIssueRef_ExtIssue(self):
+ """ConvertIssueRef successfully converts an external issue."""
+ actual = converters.ConvertIssueRef(('', 0), ext_id='b/1234567')
+ self.assertEqual(
+ common_pb2.IssueRef(project_name='', local_id=0,
+ ext_identifier='b/1234567'),
+ actual)
+
+ def testConvertIssueRefs(self):
+ """We can convert issue_ids to IssueRefs."""
+ related_refs_dict = {
+ 78901: ('proj', 1),
+ 78902: ('proj', 2),
+ }
+ actual = converters.ConvertIssueRefs([78901, 78902], related_refs_dict)
+ self.assertEqual(
+ [common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)],
+ actual)
+
+ def testConvertFieldType(self):
+ self.assertEqual(
+ common_pb2.STR_TYPE,
+ converters.ConvertFieldType(tracker_pb2.FieldTypes.STR_TYPE))
+
+ self.assertEqual(
+ common_pb2.URL_TYPE,
+ converters.ConvertFieldType(tracker_pb2.FieldTypes.URL_TYPE))
+
+ def testConvertFieldRef(self):
+ actual = converters.ConvertFieldRef(
+ 1, 'SomeName', tracker_pb2.FieldTypes.ENUM_TYPE, None)
+ self.assertEqual(
+ actual,
+ common_pb2.FieldRef(
+ field_id=1,
+ field_name='SomeName',
+ type=common_pb2.ENUM_TYPE))
+
+ def testConvertFieldValue(self):
+ """We can convert one FieldValueView item to a protoc FieldValue."""
+ actual = converters.ConvertFieldValue(
+ 1, 'Size', 123, tracker_pb2.FieldTypes.INT_TYPE, phase_name='Canary')
+ expected = issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=1,
+ field_name='Size',
+ type=common_pb2.INT_TYPE),
+ value='123',
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Canary'))
+ self.assertEqual(expected, actual)
+
+ actual = converters.ConvertFieldValue(
+ 1, 'Size', 123, tracker_pb2.FieldTypes.INT_TYPE, 'Legal', '',
+ is_derived=True)
+ expected = issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=1,
+ field_name='Size',
+ type=common_pb2.INT_TYPE,
+ approval_name='Legal'),
+ value='123',
+ is_derived=True)
+ self.assertEqual(expected, actual)
+
+ def testConvertFieldValue_Unicode(self):
+ """We can convert one FieldValueView unicode item to a protoc FieldValue."""
+ actual = converters.ConvertFieldValue(
+ 1, 'Size', u'\xe2\x9d\xa4\xef\xb8\x8f',
+ tracker_pb2.FieldTypes.STR_TYPE, phase_name='Canary')
+ expected = issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=1,
+ field_name='Size',
+ type=common_pb2.STR_TYPE),
+ value=u'\xe2\x9d\xa4\xef\xb8\x8f',
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Canary'))
+ self.assertEqual(expected, actual)
+
+ def testConvertFieldValues(self):
+ self.fd_2.approval_id = 3
+ self.config.field_defs = [
+ self.fd_1, self.fd_2, self.fd_3, self.fd_4, self.fd_5]
+ fv_1 = tracker_bizobj.MakeFieldValue(
+ 1, None, 'string', None, None, None, False)
+ fv_2 = tracker_bizobj.MakeFieldValue(
+ 2, 34, None, None, None, None, False)
+ fv_3 = tracker_bizobj.MakeFieldValue(
+ 111, None, 'value', None, None, None, False)
+ labels = ['Pre-label', 'not-label-enum', 'prenot-label']
+ der_labels = ['Pre-label2']
+ phases = [tracker_pb2.Phase(name='Canary', phase_id=17)]
+ fv_1.phase_id=17
+
+ actual = converters.ConvertFieldValues(
+ self.config, labels, der_labels, [fv_1, fv_2, fv_3], {}, phases=phases)
+
+ self.maxDiff = None
+ expected = [
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=1,
+ field_name='FirstField',
+ type=common_pb2.STR_TYPE),
+ value='string',
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Canary')),
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=2,
+ field_name='SecField',
+ type=common_pb2.INT_TYPE,
+ approval_name='LegalApproval'),
+ value='34'),
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=5, field_name='Pre', type=common_pb2.ENUM_TYPE),
+ value='label'),
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=5, field_name='Pre', type=common_pb2.ENUM_TYPE),
+ value='label2', is_derived=True),
+ ]
+ self.assertItemsEqual(expected, actual)
+
+ def testConvertIssue(self):
+ """We can convert a protorpc Issue to a protoc Issue."""
+ related_refs_dict = {
+ 78901: ('proj', 1),
+ 78902: ('proj', 2),
+ }
+ now = 12345678
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI'),
+ tracker_pb2.ComponentDef(component_id=2, path='DB'),
+ ]
+ issue = fake.MakeTestIssue(
+ 789, 3, 'sum', 'New', 111, labels=['Hot'],
+ derived_labels=['Scalability'], star_count=12, reporter_id=222,
+ opened_timestamp=now, component_ids=[1], project_name='proj',
+ cc_ids=[111], derived_cc_ids=[222])
+ issue.phases = [
+ tracker_pb2.Phase(phase_id=1, name='Dev', rank=1),
+ tracker_pb2.Phase(phase_id=2, name='Beta', rank=2),
+ ]
+ issue.dangling_blocked_on_refs = [
+ tracker_pb2.DanglingIssueRef(project='dangling_proj', issue_id=1234)]
+ issue.dangling_blocking_refs = [
+ tracker_pb2.DanglingIssueRef(project='dangling_proj', issue_id=5678)]
+
+ actual = converters.ConvertIssue(
+ issue, self.users_by_id, related_refs_dict, self.config)
+
+ expected = issue_objects_pb2.Issue(
+ project_name='proj',
+ local_id=3,
+ summary='sum',
+ status_ref=common_pb2.StatusRef(
+ status='New',
+ is_derived=False,
+ means_open=True),
+ owner_ref=common_pb2.UserRef(
+ user_id=111,
+ display_name='one@example.com',
+ is_derived=False),
+ cc_refs=[
+ common_pb2.UserRef(
+ user_id=111,
+ display_name='one@example.com',
+ is_derived=False),
+ common_pb2.UserRef(
+ user_id=222,
+ display_name='two@example.com',
+ is_derived=True)],
+ label_refs=[
+ common_pb2.LabelRef(label='Hot', is_derived=False),
+ common_pb2.LabelRef(label='Scalability', is_derived=True)],
+ component_refs=[common_pb2.ComponentRef(path='UI', is_derived=False)],
+ is_deleted=False,
+ reporter_ref=common_pb2.UserRef(
+ user_id=222, display_name='two@example.com', is_derived=False),
+ opened_timestamp=now,
+ component_modified_timestamp=now,
+ status_modified_timestamp=now,
+ owner_modified_timestamp=now,
+ star_count=12,
+ is_spam=False,
+ attachment_count=0,
+ dangling_blocked_on_refs=[
+ common_pb2.IssueRef(project_name='dangling_proj', local_id=1234)],
+ dangling_blocking_refs=[
+ common_pb2.IssueRef(project_name='dangling_proj', local_id=5678)],
+ phases=[
+ issue_objects_pb2.PhaseDef(
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Dev'),
+ rank=1),
+ issue_objects_pb2.PhaseDef(
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Beta'),
+ rank=2)])
+ self.assertEqual(expected, actual)
+
+ def testConvertIssue_NegativeAttachmentCount(self):
+ """We can convert a protorpc Issue to a protoc Issue."""
+ related_refs_dict = {
+ 78901: ('proj', 1),
+ 78902: ('proj', 2),
+ }
+ now = 12345678
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI'),
+ tracker_pb2.ComponentDef(component_id=2, path='DB'),
+ ]
+ issue = fake.MakeTestIssue(
+ 789, 3, 'sum', 'New', 111, labels=['Hot'],
+ derived_labels=['Scalability'], star_count=12, reporter_id=222,
+ opened_timestamp=now, component_ids=[1], project_name='proj',
+ cc_ids=[111], derived_cc_ids=[222], attachment_count=-10)
+ issue.phases = [
+ tracker_pb2.Phase(phase_id=1, name='Dev', rank=1),
+ tracker_pb2.Phase(phase_id=2, name='Beta', rank=2),
+ ]
+ issue.dangling_blocked_on_refs = [
+ tracker_pb2.DanglingIssueRef(project='dangling_proj', issue_id=1234)]
+ issue.dangling_blocking_refs = [
+ tracker_pb2.DanglingIssueRef(project='dangling_proj', issue_id=5678)]
+
+ actual = converters.ConvertIssue(
+ issue, self.users_by_id, related_refs_dict, self.config)
+
+ expected = issue_objects_pb2.Issue(
+ project_name='proj',
+ local_id=3,
+ summary='sum',
+ status_ref=common_pb2.StatusRef(
+ status='New',
+ is_derived=False,
+ means_open=True),
+ owner_ref=common_pb2.UserRef(
+ user_id=111,
+ display_name='one@example.com',
+ is_derived=False),
+ cc_refs=[
+ common_pb2.UserRef(
+ user_id=111,
+ display_name='one@example.com',
+ is_derived=False),
+ common_pb2.UserRef(
+ user_id=222,
+ display_name='two@example.com',
+ is_derived=True)],
+ label_refs=[
+ common_pb2.LabelRef(label='Hot', is_derived=False),
+ common_pb2.LabelRef(label='Scalability', is_derived=True)],
+ component_refs=[common_pb2.ComponentRef(path='UI', is_derived=False)],
+ is_deleted=False,
+ reporter_ref=common_pb2.UserRef(
+ user_id=222, display_name='two@example.com', is_derived=False),
+ opened_timestamp=now,
+ component_modified_timestamp=now,
+ status_modified_timestamp=now,
+ owner_modified_timestamp=now,
+ star_count=12,
+ is_spam=False,
+ dangling_blocked_on_refs=[
+ common_pb2.IssueRef(project_name='dangling_proj', local_id=1234)],
+ dangling_blocking_refs=[
+ common_pb2.IssueRef(project_name='dangling_proj', local_id=5678)],
+ phases=[
+ issue_objects_pb2.PhaseDef(
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Dev'),
+ rank=1),
+ issue_objects_pb2.PhaseDef(
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Beta'),
+ rank=2)])
+ self.assertEqual(expected, actual)
+
+ def testConvertIssue_ExternalMergedInto(self):
+ """ConvertIssue works on issues with external mergedinto values."""
+ issue = fake.MakeTestIssue(789, 3, 'sum', 'New', 111, project_name='proj',
+ merged_into_external='b/5678')
+ actual = converters.ConvertIssue(issue, self.users_by_id, {}, self.config)
+ expected = issue_objects_pb2.Issue(
+ project_name='proj',
+ local_id=3,
+ summary='sum',
+ merged_into_issue_ref=common_pb2.IssueRef(ext_identifier='b/5678'),
+ status_ref=common_pb2.StatusRef(
+ status='New',
+ is_derived=False,
+ means_open=True),
+ owner_ref=common_pb2.UserRef(
+ user_id=111,
+ display_name='one@example.com',
+ is_derived=False),
+ reporter_ref=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com', is_derived=False))
+
+ self.assertEqual(expected, actual)
+
+ def testConvertPhaseDef(self):
+ """We can convert a prototpc Phase to a protoc PhaseDef. """
+ phase = tracker_pb2.Phase(phase_id=1, name='phase', rank=2)
+ actual = converters.ConvertPhaseDef(phase)
+ expected = issue_objects_pb2.PhaseDef(
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='phase'),
+ rank=2
+ )
+ self.assertEqual(expected, actual)
+
+ def testConvertAmendment(self):
+ """We can convert various kinds of Amendments."""
+ amend = tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.SUMMARY, newvalue='new', oldvalue='old')
+ actual = converters.ConvertAmendment(amend, self.users_by_id)
+ self.assertEqual('Summary', actual.field_name)
+ self.assertEqual('new', actual.new_or_delta_value)
+ self.assertEqual('old', actual.old_value)
+
+ amend = tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.OWNER, added_user_ids=[111])
+ actual = converters.ConvertAmendment(amend, self.users_by_id)
+ self.assertEqual('Owner', actual.field_name)
+ self.assertEqual('one@example.com', actual.new_or_delta_value)
+ self.assertEqual('', actual.old_value)
+
+ amend = tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.CC,
+ added_user_ids=[111], removed_user_ids=[222])
+ actual = converters.ConvertAmendment(amend, self.users_by_id)
+ self.assertEqual('Cc', actual.field_name)
+ self.assertEqual(
+ '-two@example.com one@example.com', actual.new_or_delta_value)
+ self.assertEqual('', actual.old_value)
+
+ amend = tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.CUSTOM, custom_field_name='EstDays',
+ newvalue='12')
+ actual = converters.ConvertAmendment(amend, self.users_by_id)
+ self.assertEqual('EstDays', actual.field_name)
+ self.assertEqual('12', actual.new_or_delta_value)
+ self.assertEqual('', actual.old_value)
+
+ @patch('tracker.attachment_helpers.SignAttachmentID')
+ def testConvertAttachment(self, mock_SignAttachmentID):
+ mock_SignAttachmentID.return_value = 2
+ attach = tracker_pb2.Attachment(
+ attachment_id=1, mimetype='image/png', filename='example.png',
+ filesize=12345)
+
+ actual = converters.ConvertAttachment(attach, 'proj')
+
+ expected = issue_objects_pb2.Attachment(
+ attachment_id=1, filename='example.png',
+ size=12345, content_type='image/png',
+ thumbnail_url='attachment?aid=1&signed_aid=2&inline=1&thumb=1',
+ view_url='attachment?aid=1&signed_aid=2&inline=1',
+ download_url='attachment?aid=1&signed_aid=2')
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_Normal(self):
+ """We can convert a protorpc IssueComment to a protoc Comment."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12)
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', is_spam=False)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_CanReportComment(self):
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12)
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([permissions.FLAG_SPAM]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', can_flag=True)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_CanUnReportComment(self):
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12)
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [111], {}, 111,
+ permissions.PermissionSet([permissions.FLAG_SPAM]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', is_spam=True, is_deleted=True,
+ can_flag=True)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_CantUnFlagCommentWithoutVerdictSpam(self):
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, is_spam=True)
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [111], {}, 111,
+ permissions.PermissionSet([permissions.FLAG_SPAM]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12,
+ timestamp=now, is_spam=True, is_deleted=True)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_CanFlagSpamComment(self):
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12)
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([permissions.VERDICT_SPAM]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', can_flag=True)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_CanUnFlagSpamComment(self):
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, is_spam=True)
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [222], {}, 111,
+ permissions.PermissionSet([permissions.VERDICT_SPAM]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', is_spam=True, is_deleted=True,
+ can_flag=True)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_DeletedComment(self):
+ """We can convert a protorpc IssueComment to a protoc Comment."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, deleted_by=111)
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([permissions.DELETE_OWN]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=True,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', can_delete=True)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_DeletedCommentCantView(self):
+ """We can convert a protorpc IssueComment to a protoc Comment."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, deleted_by=111)
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=True,
+ timestamp=now)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_CommentByBannedUser(self):
+ """We can convert a protorpc IssueComment to a protoc Comment."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=333, timestamp=now,
+ content='a comment', sequence=12)
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=True,
+ timestamp=now)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_Description(self):
+ """We can convert a protorpc IssueComment to a protoc Comment."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, is_description=True)
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {101: 1}, 111,
+ permissions.PermissionSet([]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', is_spam=False, description_num=1)
+ self.assertEqual(expected, actual)
+ comment.is_description = False
+
+ def testConvertComment_Approval(self):
+ """We can convert a protorpc IssueComment to a protoc Comment."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, approval_id=11)
+ # Comment on an approval.
+ self.config.field_defs.append(tracker_pb2.FieldDef(
+ field_id=11, project_id=789, field_name='Accessibility',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ applicable_type='Launch'))
+ self.config.approval_defs.append(tracker_pb2.ApprovalDef(
+ approval_id=11, approver_ids=[111], survey='survey 1'))
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', is_spam=False,
+ approval_ref=common_pb2.FieldRef(field_name='Accessibility'))
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_ViewOwnInboundMessage(self):
+ """Users can view their own inbound messages."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, inbound_message='inbound message')
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', inbound_message='inbound message')
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_ViewInboundMessageWithPermission(self):
+ """Users can view their own inbound messages."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, inbound_message='inbound message')
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 222,
+ permissions.PermissionSet([permissions.VIEW_INBOUND_MESSAGES]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', inbound_message='inbound message')
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_NotAllowedToViewInboundMessage(self):
+ """Users can view their own inbound messages."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, inbound_message='inbound message')
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 222,
+ permissions.PermissionSet([]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment')
+ self.assertEqual(expected, actual)
+
+ def testConvertCommentList(self):
+ """We can convert a list of comments."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment_0 = tracker_pb2.IssueComment(
+ id=100, project_id=789, user_id=111, timestamp=now,
+ content='a description', sequence=0, is_description=True)
+ comment_1 = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=222, timestamp=now,
+ content='a comment', sequence=1)
+ comment_2 = tracker_pb2.IssueComment(
+ id=102, project_id=789, user_id=222, timestamp=now,
+ content='deleted comment', sequence=2, deleted_by=111)
+ comment_3 = tracker_pb2.IssueComment(
+ id=103, project_id=789, user_id=111, timestamp=now,
+ content='another desc', sequence=3, is_description=True)
+
+ actual = converters.ConvertCommentList(
+ issue, [comment_0, comment_1, comment_2, comment_3], self.config,
+ self.users_by_id, {}, 222,
+ permissions.PermissionSet([permissions.DELETE_OWN]))
+
+ expected_0 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=0, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a description', is_spam=False,
+ description_num=1)
+ expected_1 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=1, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=222, display_name='two@example.com'),
+ timestamp=now, content='a comment', is_spam=False, can_delete=True)
+ expected_2 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=2, is_deleted=True,
+ timestamp=now)
+ expected_3 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=3, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='another desc', is_spam=False,
+ description_num=2)
+ self.assertEqual(expected_0, actual[0])
+ self.assertEqual(expected_1, actual[1])
+ self.assertEqual(expected_2, actual[2])
+ self.assertEqual(expected_3, actual[3])
+
+ def testConvertCommentList_DontUseDeletedOrSpamDescriptions(self):
+ """When converting comments, deleted or spam are not descriptions."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment_0 = tracker_pb2.IssueComment(
+ id=100, project_id=789, user_id=111, timestamp=now,
+ content='a description', sequence=0, is_description=True)
+ comment_1 = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=222, timestamp=now,
+ content='a spam description', sequence=1, is_description=True,
+ is_spam=True)
+ comment_2 = tracker_pb2.IssueComment(
+ id=102, project_id=789, user_id=222, timestamp=now,
+ content='a deleted description', sequence=2, is_description=True,
+ deleted_by=111)
+ comment_3 = tracker_pb2.IssueComment(
+ id=103, project_id=789, user_id=111, timestamp=now,
+ content='another good desc', sequence=3, is_description=True)
+ comment_4 = tracker_pb2.IssueComment(
+ id=104, project_id=789, user_id=333, timestamp=now,
+ content='desc from banned', sequence=4, is_description=True)
+
+ actual = converters.ConvertCommentList(
+ issue, [comment_0, comment_1, comment_2, comment_3, comment_4],
+ self.config, self.users_by_id, {}, 222,
+ permissions.PermissionSet([permissions.DELETE_OWN]))
+
+ expected_0 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=0, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a description', is_spam=False,
+ description_num=1)
+ expected_1 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=1, is_deleted=True,
+ timestamp=now, is_spam=True, can_delete=False)
+ expected_2 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=2, is_deleted=True,
+ timestamp=now)
+ expected_3 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=3, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='another good desc', is_spam=False,
+ description_num=2)
+ expected_4 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=4, is_deleted=True,
+ timestamp=now, is_spam=False)
+ self.assertEqual(expected_0, actual[0])
+ self.assertEqual(expected_1, actual[1])
+ self.assertEqual(expected_2, actual[2])
+ self.assertEqual(expected_3, actual[3])
+ self.assertEqual(expected_4, actual[4])
+
+ def testIngestUserRef(self):
+ """We can look up a single user ID for a protoc UserRef."""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ ref = common_pb2.UserRef(display_name='user1@example.com')
+ actual = converters.IngestUserRef(self.cnxn, ref, self.services.user)
+ self.assertEqual(111, actual)
+
+ def testIngestUserRef_NoSuchUser(self):
+ """We reject a malformed UserRef.display_name."""
+ ref = common_pb2.UserRef(display_name='Bob@gmail.com')
+ with self.assertRaises(exceptions.NoSuchUserException):
+ converters.IngestUserRef(self.cnxn, ref, self.services.user)
+
+ def testIngestUserRefs_ClearTheOwnerField(self):
+ """We can look up user IDs for protoc UserRefs."""
+ ref = common_pb2.UserRef(user_id=0)
+ actual = converters.IngestUserRefs(self.cnxn, [ref], self.services.user)
+ self.assertEqual([0], actual)
+
+ def testIngestUserRefs_ByExistingID(self):
+ """Users can be specified by user_id."""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ ref = common_pb2.UserRef(user_id=111)
+ actual = converters.IngestUserRefs(self.cnxn, [ref], self.services.user)
+ self.assertEqual([111], actual)
+
+ def testIngestUserRefs_ByNonExistingID(self):
+ """We reject references to non-existing user IDs."""
+ ref = common_pb2.UserRef(user_id=999)
+ with self.assertRaises(exceptions.NoSuchUserException):
+ converters.IngestUserRefs(self.cnxn, [ref], self.services.user)
+
+ def testIngestUserRefs_ByExistingEmail(self):
+ """Existing users can be specified by email address."""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ ref = common_pb2.UserRef(display_name='user1@example.com')
+ actual = converters.IngestUserRefs(self.cnxn, [ref], self.services.user)
+ self.assertEqual([111], actual)
+
+ def testIngestUserRefs_ByNonExistingEmail(self):
+ """New users can be specified by email address."""
+ # Case where autocreate=False
+ ref = common_pb2.UserRef(display_name='new@example.com')
+ with self.assertRaises(exceptions.NoSuchUserException):
+ converters.IngestUserRefs(
+ self.cnxn, [ref], self.services.user, autocreate=False)
+
+ # Case where autocreate=True
+ actual = converters.IngestUserRefs(
+ self.cnxn, [ref], self.services.user, autocreate=True)
+ user_id = self.services.user.LookupUserID(self.cnxn, 'new@example.com')
+ self.assertEqual([user_id], actual)
+
+ def testIngestUserRefs_ByMalformedEmail(self):
+ """We ignore malformed user emails."""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ self.services.user.TestAddUser('user3@example.com', 333)
+ refs = [
+ common_pb2.UserRef(user_id=0),
+ common_pb2.UserRef(display_name='not-a-valid-email'),
+ common_pb2.UserRef(user_id=333),
+ common_pb2.UserRef(display_name='user1@example.com')
+ ]
+ actual = converters.IngestUserRefs(
+ self.cnxn, refs, self.services.user, autocreate=True)
+ self.assertEqual(actual, [0, 333, 111])
+
+ def testIngestUserRefs_MixOfIDAndEmail(self):
+ """Requests can specify some users by ID and others by email."""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ self.services.user.TestAddUser('user2@example.com', 222)
+ self.services.user.TestAddUser('user3@example.com', 333)
+ ref1 = common_pb2.UserRef(display_name='user1@example.com')
+ ref2 = common_pb2.UserRef(display_name='user2@example.com')
+ ref3 = common_pb2.UserRef(user_id=333)
+ actual = converters.IngestUserRefs(
+ self.cnxn, [ref1, ref2, ref3], self.services.user)
+ self.assertEqual([111, 222, 333], actual)
+
+ def testIngestUserRefs_UppercaseEmail(self):
+ """Request can include uppercase letters in email"""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ ref = common_pb2.UserRef(display_name='USER1@example.com')
+ actual = converters.IngestUserRefs(self.cnxn, [ref], self.services.user)
+ self.assertEqual([111], actual)
+
+ def testIngestPrefValues(self):
+ """We can convert a list of UserPrefValues from protoc to protorpc."""
+ self.assertEqual(
+ [],
+ converters.IngestPrefValues([]))
+
+ userprefvalues = [
+ user_objects_pb2.UserPrefValue(name='foo_1', value='bar_1'),
+ user_objects_pb2.UserPrefValue(name='foo_2', value='bar_2')]
+ actual = converters.IngestPrefValues(userprefvalues)
+ expected = [
+ user_pb2.UserPrefValue(name='foo_1', value='bar_1'),
+ user_pb2.UserPrefValue(name='foo_2', value='bar_2')]
+ self.assertEqual(expected, actual)
+
+ def testIngestComponentRefs(self):
+ """We can look up component IDs for a list of protoc UserRefs."""
+ self.assertEqual([], converters.IngestComponentRefs([], self.config))
+
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI'),
+ tracker_pb2.ComponentDef(component_id=2, path='DB')]
+ refs = [common_pb2.ComponentRef(path='UI'),
+ common_pb2.ComponentRef(path='DB')]
+ self.assertEqual(
+ [1, 2], converters.IngestComponentRefs(refs, self.config))
+
+ def testIngestIssueRefs_ValidatesExternalRefs(self):
+ """IngestIssueRefs requires external refs have at least one slash."""
+ ref = common_pb2.IssueRef(ext_identifier='b123456')
+ with self.assertRaises(exceptions.InvalidExternalIssueReference):
+ converters.IngestIssueRefs(self.cnxn, [ref], self.services)
+
+ def testIngestIssueRefs_SkipsExternalRefs(self):
+ """IngestIssueRefs skips external refs."""
+ ref = common_pb2.IssueRef(ext_identifier='b/123456')
+ actual = converters.IngestIssueRefs(
+ self.cnxn, [ref], self.services)
+ self.assertEqual([], actual)
+
+ def testIngestExtIssueRefs_Normal(self):
+ """IngestExtIssueRefs returns all valid external refs."""
+ refs = [
+ common_pb2.IssueRef(project_name='rutabaga', local_id=1234),
+ common_pb2.IssueRef(ext_identifier='b123456'),
+ common_pb2.IssueRef(ext_identifier='b/123456'), # <- Valid ref 1.
+ common_pb2.IssueRef(ext_identifier='rutabaga/123456'),
+ common_pb2.IssueRef(ext_identifier='123456'),
+ common_pb2.IssueRef(ext_identifier='b/56789'), # <- Valid ref 2.
+ common_pb2.IssueRef(ext_identifier='b//123456')]
+
+ actual = converters.IngestExtIssueRefs(refs)
+ self.assertEqual(['b/123456', 'b/56789'], actual)
+
+ def testIngestIssueDelta_Empty(self):
+ """An empty protorpc IssueDelta makes an empty protoc IssueDelta."""
+ delta = issue_objects_pb2.IssueDelta()
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+ expected = tracker_pb2.IssueDelta()
+ self.assertEqual(expected, actual)
+
+ def testIngestIssueDelta_BuiltInFields(self):
+ """We can create a protorpc IssueDelta from a protoc IssueDelta."""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ self.services.user.TestAddUser('user2@example.com', 222)
+ self.services.user.TestAddUser('user3@example.com', 333)
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI')]
+ delta = issue_objects_pb2.IssueDelta(
+ status=wrappers_pb2.StringValue(value='Fixed'),
+ owner_ref=common_pb2.UserRef(user_id=222),
+ summary=wrappers_pb2.StringValue(value='New summary'),
+ cc_refs_add=[common_pb2.UserRef(user_id=333)],
+ comp_refs_add=[common_pb2.ComponentRef(path='UI')],
+ label_refs_add=[common_pb2.LabelRef(label='Hot')])
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+ expected = tracker_pb2.IssueDelta(
+ status='Fixed', owner_id=222, summary='New summary',
+ cc_ids_add=[333], comp_ids_add=[1],
+ labels_add=['Hot'])
+ self.assertEqual(expected, actual)
+
+ def testIngestIssueDelta_ClearMergedInto(self):
+ """We can clear merged into from the current issue."""
+ delta = issue_objects_pb2.IssueDelta(merged_into_ref=common_pb2.IssueRef())
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+ expected = tracker_pb2.IssueDelta(merged_into=0)
+ self.assertEqual(expected, actual)
+
+ def testIngestIssueDelta_BadOwner(self):
+ """We reject a specified owner that does not exist."""
+ delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(display_name='user@exa'))
+ with self.assertRaises(exceptions.NoSuchUserException):
+ converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+
+ def testIngestIssueDelta_BadOwnerIgnored(self):
+ """We can ignore an incomplete owner email for presubmit."""
+ delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(display_name='user@exa'))
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [],
+ ignore_missing_objects=True)
+ expected = tracker_pb2.IssueDelta()
+ self.assertEqual(expected, actual)
+
+ def testIngestIssueDelta_InvalidComponent(self):
+ """We reject a protorpc IssueDelta that has an invalid component."""
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI')]
+ delta = issue_objects_pb2.IssueDelta(
+ comp_refs_add=[common_pb2.ComponentRef(path='XYZ')])
+ with self.assertRaises(exceptions.NoSuchComponentException):
+ converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+
+ def testIngestIssueDelta_InvalidComponentIgnored(self):
+ """We can ignore invalid components for presubmits."""
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI')]
+ delta = issue_objects_pb2.IssueDelta(
+ comp_refs_add=[common_pb2.ComponentRef(path='UI'),
+ common_pb2.ComponentRef(path='XYZ')])
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [],
+ ignore_missing_objects=True)
+ self.assertEqual([1], actual.comp_ids_add)
+
+ def testIngestIssueDelta_CustomFields(self):
+ """We can create a protorpc IssueDelta from a protoc IssueDelta."""
+ self.config.field_defs = [
+ self.fd_1, self.fd_2, self.fd_3, self.fd_4, self.fd_6]
+ phases = [tracker_pb2.Phase(phase_id=1, name="Beta")]
+ delta = issue_objects_pb2.IssueDelta(
+ field_vals_add=[
+ issue_objects_pb2.FieldValue(
+ value='string',
+ field_ref=common_pb2.FieldRef(field_name='FirstField')
+ ),
+ issue_objects_pb2.FieldValue(
+ value='1',
+ field_ref=common_pb2.FieldRef(field_name='PhaseField'),
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Beta')
+ )],
+ field_vals_remove=[
+ issue_objects_pb2.FieldValue(
+ value='34', field_ref=common_pb2.FieldRef(
+ field_name='SecField'))],
+ fields_clear=[common_pb2.FieldRef(field_name='FirstField')])
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, phases)
+ self.assertEqual(actual.field_vals_add,
+ [tracker_pb2.FieldValue(
+ str_value='string', field_id=1, derived=False),
+ tracker_pb2.FieldValue(
+ int_value=1, field_id=6, phase_id=1, derived=False)
+ ])
+ self.assertEqual(actual.field_vals_remove, [tracker_pb2.FieldValue(
+ int_value=34, field_id=2, derived=False)])
+ self.assertEqual(actual.fields_clear, [1])
+
+ def testIngestIssueDelta_InvalidCustomFields(self):
+ """We can create a protorpc IssueDelta from a protoc IssueDelta."""
+ # TODO(jrobbins): add and remove.
+ delta = issue_objects_pb2.IssueDelta(
+ fields_clear=[common_pb2.FieldRef(field_name='FirstField')])
+ with self.assertRaises(exceptions.NoSuchFieldDefException):
+ converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+
+ def testIngestIssueDelta_ShiftFieldsIntoLabels(self):
+ """Test that enum fields are shifted into labels."""
+ self.config.field_defs = [self.fd_5]
+ delta = issue_objects_pb2.IssueDelta(
+ field_vals_add=[
+ issue_objects_pb2.FieldValue(
+ value='Foo',
+ field_ref=common_pb2.FieldRef(field_name='Pre', field_id=5)
+ )],
+ field_vals_remove=[
+ issue_objects_pb2.FieldValue(
+ value='Bar',
+ field_ref=common_pb2.FieldRef(field_name='Pre', field_id=5),
+ )])
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+ self.assertEqual(actual.field_vals_add, [])
+ self.assertEqual(actual.field_vals_remove, [])
+ self.assertEqual(actual.labels_add, ['Pre-Foo'])
+ self.assertEqual(actual.labels_remove, ['Pre-Bar'])
+
+ def testIngestIssueDelta_RelatedIssues(self):
+ """We can create a protorpc IssueDelta that references related issues."""
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111)
+ self.services.issue.TestAddIssue(issue)
+ delta = issue_objects_pb2.IssueDelta(
+ blocked_on_refs_add=[common_pb2.IssueRef(
+ project_name='proj', local_id=issue.local_id)],
+ merged_into_ref=common_pb2.IssueRef(
+ project_name='proj', local_id=issue.local_id))
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+ self.assertEqual([issue.issue_id], actual.blocked_on_add)
+ self.assertEqual([], actual.blocking_add)
+ self.assertEqual(issue.issue_id, actual.merged_into)
+
+ def testIngestIssueDelta_InvalidRelatedIssues(self):
+ """We reject references to related issues that do not exist."""
+ delta = issue_objects_pb2.IssueDelta(
+ merged_into_ref=common_pb2.IssueRef(
+ project_name='not-a-proj', local_id=8))
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+
+ delta = issue_objects_pb2.IssueDelta(
+ merged_into_ref=common_pb2.IssueRef(
+ project_name='proj', local_id=999))
+ with self.assertRaises(exceptions.NoSuchIssueException):
+ converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+
+ def testIngestIssueDelta_ExternalMergedInto(self):
+ """IngestIssueDelta properly handles external mergedinto refs."""
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111)
+ self.services.issue.TestAddIssue(issue)
+ delta = issue_objects_pb2.IssueDelta(
+ merged_into_ref=common_pb2.IssueRef(ext_identifier='b/5678'))
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+
+ self.assertIsNone(actual.merged_into)
+ self.assertEqual('b/5678', actual.merged_into_external)
+
+ def testIngestAttachmentUploads_Empty(self):
+ """Uploading zero files results in an empty list of attachments."""
+ self.assertEqual([], converters.IngestAttachmentUploads([]))
+
+ def testIngestAttachmentUploads_Normal(self):
+ """Uploading files results in a list of attachments."""
+ uploads = [
+ issue_objects_pb2.AttachmentUpload(
+ filename='hello.c', content='int main() {}'),
+ issue_objects_pb2.AttachmentUpload(
+ filename='README.md', content='readme content'),
+ ]
+ actual = converters.IngestAttachmentUploads(uploads)
+ self.assertEqual(
+ [('hello.c', 'int main() {}', 'text/plain'),
+ ('README.md', 'readme content', 'text/plain')],
+ actual)
+
+ def testIngestAttachmentUploads_Invalid(self):
+ """We reject uploaded files that lack a name or content."""
+ with self.assertRaises(exceptions.InputException):
+ converters.IngestAttachmentUploads([
+ issue_objects_pb2.AttachmentUpload(content='name is mssing')])
+
+ with self.assertRaises(exceptions.InputException):
+ converters.IngestAttachmentUploads([
+ issue_objects_pb2.AttachmentUpload(filename='content is mssing')])
+
+ def testIngestApprovalDelta(self):
+ self.services.user.TestAddUser('user1@example.com', 111)
+ self.services.user.TestAddUser('user2@example.com', 222)
+
+ self.config.field_defs = [
+ self.fd_1, self.fd_2, self.fd_3, self.fd_4, self.fd_7]
+
+ approval_delta = issue_objects_pb2.ApprovalDelta(
+ status=issue_objects_pb2.APPROVED,
+ approver_refs_add=[common_pb2.UserRef(user_id=111)],
+ approver_refs_remove=[common_pb2.UserRef(user_id=222)],
+ field_vals_add=[
+ issue_objects_pb2.FieldValue(
+ value='string', field_ref=common_pb2.FieldRef(
+ field_id=1, field_name='FirstField')),
+ issue_objects_pb2.FieldValue(
+ value='choice1', field_ref=common_pb2.FieldRef(
+ field_id=7, field_name='ApprovalEnum')),
+ ],
+ field_vals_remove=[
+ issue_objects_pb2.FieldValue(
+ value='34', field_ref=common_pb2.FieldRef(
+ field_id=2, field_name='SecField')),
+ issue_objects_pb2.FieldValue(
+ value='choice2', field_ref=common_pb2.FieldRef(
+ field_id=7, field_name='ApprovalEnum')),
+ ],
+ fields_clear=[common_pb2.FieldRef(field_name='FirstField')])
+
+ actual = converters.IngestApprovalDelta(
+ self.cnxn, self.services.user, approval_delta, 333, self.config)
+ self.assertEqual(
+ actual.status, tracker_pb2.ApprovalStatus.APPROVED,)
+ self.assertEqual(actual.setter_id, 333)
+ self.assertEqual(actual.approver_ids_add, [111])
+ self.assertEqual(actual.approver_ids_remove, [222])
+ self.assertEqual(actual.subfield_vals_add, [tracker_pb2.FieldValue(
+ str_value='string', field_id=1, derived=False)])
+ self.assertEqual(actual.subfield_vals_remove, [tracker_pb2.FieldValue(
+ int_value=34, field_id=2, derived=False)])
+ self.assertEqual(actual.subfields_clear, [1])
+ self.assertEqual(actual.labels_add, ['ApprovalEnum-choice1'])
+ self.assertEqual(actual.labels_remove, ['ApprovalEnum-choice2'])
+
+ # test a NOT_SET status is registered as None.
+ approval_delta.status = issue_objects_pb2.NOT_SET
+ actual = converters.IngestApprovalDelta(
+ self.cnxn, self.services.user, approval_delta, 333, self.config)
+ self.assertIsNone(actual.status)
+
+ def testIngestApprovalStatus(self):
+ actual = converters.IngestApprovalStatus(issue_objects_pb2.NOT_SET)
+ self.assertEqual(actual, tracker_pb2.ApprovalStatus.NOT_SET)
+
+ actual = converters.IngestApprovalStatus(issue_objects_pb2.NOT_APPROVED)
+ self.assertEqual(actual, tracker_pb2.ApprovalStatus.NOT_APPROVED)
+
+ def testIngestFieldValues(self):
+ self.services.user.TestAddUser('user1@example.com', 111)
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_4, self.fd_6]
+ phases = [
+ tracker_pb2.Phase(phase_id=3, name="Dev"),
+ tracker_pb2.Phase(phase_id=1, name="Beta")
+ ]
+
+ field_values = [
+ issue_objects_pb2.FieldValue(
+ value='string',
+ field_ref=common_pb2.FieldRef(field_name='FirstField')
+ ),
+ issue_objects_pb2.FieldValue(
+ value='34',
+ field_ref=common_pb2.FieldRef(field_name='SecField')
+ ),
+ issue_objects_pb2.FieldValue(
+ value='user1@example.com',
+ field_ref=common_pb2.FieldRef(field_name='UserField'),
+ # phase_ref for non-phase fields should be ignored.
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Dev')
+ ),
+ issue_objects_pb2.FieldValue(
+ value='2',
+ field_ref=common_pb2.FieldRef(field_name='PhaseField'),
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Beta'))
+ ]
+
+ actual = converters.IngestFieldValues(
+ self.cnxn, self.services.user, field_values, self.config, phases)
+ self.assertEqual(
+ actual,
+ [
+ tracker_pb2.FieldValue(
+ str_value='string', field_id=1, derived=False),
+ tracker_pb2.FieldValue(int_value=34, field_id=2, derived=False),
+ tracker_pb2.FieldValue(user_id=111, field_id=4, derived=False),
+ tracker_pb2.FieldValue(
+ int_value=2, field_id=6, phase_id=1, derived=False)
+ ]
+ )
+
+ def testIngestFieldValues_EmptyUser(self):
+ """We ignore empty user email strings."""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_4, self.fd_6]
+ field_values = [
+ issue_objects_pb2.FieldValue(
+ value='user1@example.com',
+ field_ref=common_pb2.FieldRef(field_name='UserField')),
+ issue_objects_pb2.FieldValue(
+ value='',
+ field_ref=common_pb2.FieldRef(field_name='UserField'))
+ ]
+
+ actual = converters.IngestFieldValues(
+ self.cnxn, self.services.user, field_values, self.config, [])
+ self.assertEqual(
+ actual,
+ [tracker_pb2.FieldValue(user_id=111, field_id=4, derived=False)])
+
+ def testIngestFieldValues_Unicode(self):
+ """We can ingest unicode strings."""
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_4, self.fd_6]
+ field_values = [
+ issue_objects_pb2.FieldValue(
+ value=u'\xe2\x9d\xa4\xef\xb8\x8f',
+ field_ref=common_pb2.FieldRef(field_name='FirstField')
+ ),
+ ]
+
+ actual = converters.IngestFieldValues(
+ self.cnxn, self.services.user, field_values, self.config, [])
+ self.assertEqual(
+ actual,
+ [
+ tracker_pb2.FieldValue(
+ str_value=u'\xe2\x9d\xa4\xef\xb8\x8f', field_id=1,
+ derived=False),
+ ]
+ )
+
+ def testIngestFieldValues_InvalidUser(self):
+ """We reject invalid user email strings."""
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_4, self.fd_6]
+ field_values = [
+ issue_objects_pb2.FieldValue(
+ value='bad value',
+ field_ref=common_pb2.FieldRef(field_name='UserField'))]
+
+ with self.assertRaises(exceptions.NoSuchUserException):
+ converters.IngestFieldValues(
+ self.cnxn, self.services.user, field_values, self.config, [])
+
+ def testIngestFieldValues_InvalidInt(self):
+ """We reject invalid int-field strings."""
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_4, self.fd_6]
+ field_values = [
+ issue_objects_pb2.FieldValue(
+ value='Not a number',
+ field_ref=common_pb2.FieldRef(field_name='SecField'))]
+
+ with self.assertRaises(exceptions.InputException) as cm:
+ converters.IngestFieldValues(
+ self.cnxn, self.services.user, field_values, self.config, [])
+
+ self.assertEqual(
+ 'Unparsable value for field SecField',
+ cm.exception.message)
+
+ def testIngestSavedQueries(self):
+ self.services.project.TestAddProject('chromium', project_id=1)
+ self.services.project.TestAddProject('fakeproject', project_id=2)
+
+ saved_queries = [
+ tracker_pb2.SavedQuery(
+ query_id=101,
+ name='test query',
+ query='owner:me',
+ executes_in_project_ids=[1, 2]),
+ tracker_pb2.SavedQuery(
+ query_id=202,
+ name='another query',
+ query='-component:Test',
+ executes_in_project_ids=[1])
+ ]
+
+ converted_queries = converters.IngestSavedQueries(self.cnxn,
+ self.services.project, saved_queries)
+
+ self.assertEqual(converted_queries[0].query_id, 101)
+ self.assertEqual(converted_queries[0].name, 'test query')
+ self.assertEqual(converted_queries[0].query, 'owner:me')
+ self.assertEqual(converted_queries[0].project_names,
+ ['chromium', 'fakeproject'])
+
+ self.assertEqual(converted_queries[1].query_id, 202)
+ self.assertEqual(converted_queries[1].name, 'another query')
+ self.assertEqual(converted_queries[1].query, '-component:Test')
+ self.assertEqual(converted_queries[1].project_names, ['chromium'])
+
+
+ def testIngestHotlistRef(self):
+ self.services.user.TestAddUser('user1@example.com', 111)
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ owner_ref = common_pb2.UserRef(user_id=111)
+ hotlist_ref = common_pb2.HotlistRef(name='Fake-Hotlist', owner=owner_ref)
+
+ actual_hotlist_id = converters.IngestHotlistRef(
+ self.cnxn, self.services.user, self.services.features, hotlist_ref)
+ self.assertEqual(actual_hotlist_id, hotlist.hotlist_id)
+
+ def testIngestHotlistRef_HotlistID(self):
+ self.services.user.TestAddUser('user1@example.com', 111)
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ hotlist_ref = common_pb2.HotlistRef(hotlist_id=hotlist.hotlist_id)
+
+ actual_hotlist_id = converters.IngestHotlistRef(
+ self.cnxn, self.services.user, self.services.features, hotlist_ref)
+ self.assertEqual(actual_hotlist_id, hotlist.hotlist_id)
+
+ def testIngestHotlistRef_NotEnoughInformation(self):
+ hotlist_ref = common_pb2.HotlistRef(name='Some-Hotlist')
+ with self.assertRaises(features_svc.NoSuchHotlistException):
+ converters.IngestHotlistRef(
+ self.cnxn, self.services.user, self.services.features, hotlist_ref)
+
+ def testIngestHotlistRef_InconsistentRequest(self):
+ self.services.user.TestAddUser('user1@example.com', 111)
+ hotlist1 = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist-2', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ hotlist_ref = common_pb2.HotlistRef(
+ hotlist_id=hotlist1.hotlist_id,
+ name='Fake-Hotlist-2',
+ owner=common_pb2.UserRef(user_id=111))
+ with self.assertRaises(features_svc.NoSuchHotlistException):
+ converters.IngestHotlistRef(
+ self.cnxn, self.services.user, self.services.features, hotlist_ref)
+
+ def testIngestHotlistRef_NonExistentHotlistID(self):
+ hotlist_ref = common_pb2.HotlistRef(hotlist_id=1234)
+ with self.assertRaises(features_svc.NoSuchHotlistException):
+ converters.IngestHotlistRef(
+ self.cnxn, self.services.user, self.services.features, hotlist_ref)
+
+ def testIngestHotlistRef_NoSuchHotlist(self):
+ self.services.user.TestAddUser('user1@example.com', 111)
+
+ owner_ref = common_pb2.UserRef(user_id=111)
+ hotlist_ref = common_pb2.HotlistRef(name='Fake-Hotlist', owner=owner_ref)
+
+ with self.assertRaises(features_svc.NoSuchHotlistException):
+ converters.IngestHotlistRef(
+ self.cnxn, self.services.user, self.services.features, hotlist_ref)
+
+ def testIngestHotlistRefs(self):
+ self.services.user.TestAddUser('user1@example.com', 111)
+ hotlist_1 = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ hotlist_2 = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist-2', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ owner_ref = common_pb2.UserRef(user_id=111)
+ hotlist_refs = [
+ common_pb2.HotlistRef(name='Fake-Hotlist', owner=owner_ref),
+ common_pb2.HotlistRef(hotlist_id=hotlist_2.hotlist_id)]
+
+ actual_hotlist_ids = converters.IngestHotlistRefs(
+ self.cnxn, self.services.user, self.services.features, hotlist_refs)
+ self.assertEqual(
+ actual_hotlist_ids, [hotlist_1.hotlist_id, hotlist_2.hotlist_id])
+
+ def testIngestPagination(self):
+ # Use settings.max_project_search_results_per_page if max_items is not
+ # present.
+ pagination = common_pb2.Pagination(start=1234)
+ self.assertEqual(
+ (1234, settings.max_artifact_search_results_per_page),
+ converters.IngestPagination(pagination))
+ # Otherwise, use the minimum between what was requested and
+ # settings.max_project_search_results_per_page
+ pagination = common_pb2.Pagination(start=1234, max_items=56)
+ self.assertEqual(
+ (1234, 56),
+ converters.IngestPagination(pagination))
+ pagination = common_pb2.Pagination(start=1234, max_items=5678)
+ self.assertEqual(
+ (1234, settings.max_artifact_search_results_per_page),
+ converters.IngestPagination(pagination))
+
+ # TODO(jojwang): add testConvertStatusRef
+
+ def testConvertStatusDef(self):
+ """We can convert a status definition to protoc."""
+ status_def = tracker_pb2.StatusDef(status='Started')
+ actual = converters.ConvertStatusDef(status_def)
+ self.assertEqual('Started', actual.status)
+ self.assertFalse(actual.means_open)
+ self.assertEqual('', actual.docstring)
+ self.assertFalse(actual.deprecated)
+ # rank is not set on output, only used when setting a new rank.
+ self.assertEqual(0, actual.rank)
+
+ status_def = tracker_pb2.StatusDef(
+ status='New', means_open=True, status_docstring='doc', deprecated=True)
+ actual = converters.ConvertStatusDef(status_def)
+ self.assertEqual('New', actual.status)
+ self.assertTrue(actual.means_open)
+ self.assertEqual('doc', actual.docstring)
+ self.assertTrue(actual.deprecated)
+ self.assertEqual(0, actual.rank)
+
+ def testConvertLabelDef(self):
+ """We can convert a label definition to protoc."""
+ label_def = tracker_pb2.LabelDef(label='Security')
+ actual = converters.ConvertLabelDef(label_def)
+ self.assertEqual('Security', actual.label)
+ self.assertEqual('', actual.docstring)
+ self.assertFalse(actual.deprecated)
+
+ label_def = tracker_pb2.LabelDef(
+ label='UI', label_docstring='doc', deprecated=True)
+ actual = converters.ConvertLabelDef(label_def)
+ self.assertEqual('UI', actual.label)
+ self.assertEqual('doc', actual.docstring)
+ self.assertTrue(actual.deprecated)
+
+ def testConvertComponentDef_Simple(self):
+ """We can convert a minimal component definition to protoc."""
+ now = 1234567890
+ component_def = tracker_pb2.ComponentDef(
+ path='Frontend', docstring='doc', created=now, creator_id=111,
+ modified=now + 1, modifier_id=111)
+ actual = converters.ConvertComponentDef(
+ component_def, self.users_by_id, {}, True)
+ self.assertEqual('Frontend', actual.path)
+ self.assertEqual('doc', actual.docstring)
+ self.assertFalse(actual.deprecated)
+ self.assertEqual(now, actual.created)
+ self.assertEqual(111, actual.creator_ref.user_id)
+ self.assertEqual(now + 1, actual.modified)
+ self.assertEqual(111, actual.modifier_ref.user_id)
+ self.assertEqual('one@example.com', actual.creator_ref.display_name)
+
+ def testConvertComponentDef_Normal(self):
+ """We can convert a component def that has CC'd users and adds labels."""
+ labels_by_id = {1: 'Security', 2: 'Usability'}
+ component_def = tracker_pb2.ComponentDef(
+ path='Frontend', admin_ids=[111], cc_ids=[222], label_ids=[1, 2],
+ docstring='doc')
+ actual = converters.ConvertComponentDef(
+ component_def, self.users_by_id, labels_by_id, True)
+ self.assertEqual('Frontend', actual.path)
+ self.assertEqual('doc', actual.docstring)
+ self.assertEqual(1, len(actual.admin_refs))
+ self.assertEqual(111, actual.admin_refs[0].user_id)
+ self.assertEqual(1, len(actual.cc_refs))
+ self.assertFalse(actual.deprecated)
+ self.assertEqual(222, actual.cc_refs[0].user_id)
+ self.assertEqual(2, len(actual.label_refs))
+ self.assertEqual('Security', actual.label_refs[0].label)
+ self.assertEqual('Usability', actual.label_refs[1].label)
+
+ # Without include_admin_info, some fields are not set.
+ actual = converters.ConvertComponentDef(
+ component_def, self.users_by_id, labels_by_id, False)
+ self.assertEqual('Frontend', actual.path)
+ self.assertEqual('doc', actual.docstring)
+ self.assertEqual(0, len(actual.admin_refs))
+ self.assertEqual(0, len(actual.cc_refs))
+ self.assertFalse(actual.deprecated)
+ self.assertEqual(0, len(actual.label_refs))
+
+ def testConvertFieldDef_Simple(self):
+ """We can convert a minimal field definition to protoc."""
+ field_def = tracker_pb2.FieldDef(
+ field_name='EstDays', field_type=tracker_pb2.FieldTypes.INT_TYPE)
+ actual = converters.ConvertFieldDef(
+ field_def, [], self.users_by_id, self.config, True)
+ self.assertEqual('EstDays', actual.field_ref.field_name)
+ self.assertEqual(common_pb2.INT_TYPE, actual.field_ref.type)
+ self.assertEqual('', actual.field_ref.approval_name)
+ self.assertEqual('', actual.applicable_type)
+ self.assertEqual('', actual.docstring)
+ self.assertEqual(0, len(actual.admin_refs))
+ self.assertFalse(actual.is_required)
+ self.assertFalse(actual.is_niche)
+ self.assertFalse(actual.is_multivalued)
+ self.assertFalse(actual.is_phase_field)
+
+ field_def = tracker_pb2.FieldDef(
+ field_name='DesignDocs', field_type=tracker_pb2.FieldTypes.URL_TYPE,
+ applicable_type='Enhancement', is_required=True, is_niche=True,
+ is_multivalued=True, docstring='doc', admin_ids=[111],
+ is_phase_field=True)
+ actual = converters.ConvertFieldDef(
+ field_def, [], self.users_by_id, self.config, True)
+ self.assertEqual('DesignDocs', actual.field_ref.field_name)
+ self.assertEqual(common_pb2.URL_TYPE, actual.field_ref.type)
+ self.assertEqual('', actual.field_ref.approval_name)
+ self.assertEqual('Enhancement', actual.applicable_type)
+ self.assertEqual('doc', actual.docstring)
+ self.assertEqual(1, len(actual.admin_refs))
+ self.assertEqual(111, actual.admin_refs[0].user_id)
+ self.assertTrue(actual.is_required)
+ self.assertTrue(actual.is_niche)
+ self.assertTrue(actual.is_multivalued)
+ self.assertTrue(actual.is_phase_field)
+
+ # Without include_admin_info, some fields are not set.
+ actual = converters.ConvertFieldDef(
+ field_def, [], self.users_by_id, self.config, False)
+ self.assertEqual('DesignDocs', actual.field_ref.field_name)
+ self.assertEqual(common_pb2.URL_TYPE, actual.field_ref.type)
+ self.assertEqual('', actual.field_ref.approval_name)
+ self.assertEqual('', actual.applicable_type)
+ self.assertEqual('doc', actual.docstring)
+ self.assertEqual(0, len(actual.admin_refs))
+ self.assertFalse(actual.is_required)
+ self.assertFalse(actual.is_niche)
+ self.assertFalse(actual.is_multivalued)
+ self.assertFalse(actual.is_phase_field)
+
+ def testConvertFieldDef_FieldOfAnApproval(self):
+ """We can convert a field that is part of an approval."""
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_3]
+ field_def = tracker_pb2.FieldDef(
+ field_name='Waiver', field_type=tracker_pb2.FieldTypes.URL_TYPE,
+ approval_id=self.fd_3.field_id)
+ actual = converters.ConvertFieldDef(
+ field_def, [], self.users_by_id, self.config, True)
+ self.assertEqual('Waiver', actual.field_ref.field_name)
+ self.assertEqual('LegalApproval', actual.field_ref.approval_name)
+
+ def testConvertFieldDef_UserChoices(self):
+ """We can convert an user type field that need special permissions."""
+ field_def = tracker_pb2.FieldDef(
+ field_name='PM', field_type=tracker_pb2.FieldTypes.USER_TYPE)
+ actual = converters.ConvertFieldDef(
+ field_def, [111, 333], self.users_by_id, self.config, False)
+ self.assertEqual('PM', actual.field_ref.field_name)
+ self.assertEqual(
+ [111, 333],
+ [user_ref.user_id for user_ref in actual.user_choices])
+ self.assertEqual(
+ ['one@example.com', 'banned@example.com'],
+ [user_ref.display_name for user_ref in actual.user_choices])
+
+ def testConvertFieldDef_EnumChoices(self):
+ """We can convert an enum type field."""
+ field_def = tracker_pb2.FieldDef(
+ field_name='Type', field_type=tracker_pb2.FieldTypes.ENUM_TYPE)
+ actual = converters.ConvertFieldDef(
+ field_def, [], self.users_by_id, self.config, False)
+ self.assertEqual('Type', actual.field_ref.field_name)
+ self.assertEqual(
+ ['Defect', 'Enhancement', 'Task', 'Other'],
+ [label_def.label for label_def in actual.enum_choices])
+
+ def testConvertApprovalDef(self):
+ """We can convert an ApprovalDef to protoc."""
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_3]
+ approval_def = tracker_pb2.ApprovalDef(approval_id=3)
+ actual = converters.ConvertApprovalDef(
+ approval_def, self.users_by_id, self.config, True)
+ self.assertEqual('LegalApproval', actual.field_ref.field_name)
+ self.assertEqual(common_pb2.APPROVAL_TYPE, actual.field_ref.type)
+ self.assertEqual(0, len(actual.approver_refs))
+ self.assertEqual('', actual.survey)
+
+ approval_def = tracker_pb2.ApprovalDef(
+ approval_id=3, approver_ids=[111], survey='What?')
+ actual = converters.ConvertApprovalDef(
+ approval_def, self.users_by_id, self.config, True)
+ self.assertEqual('LegalApproval', actual.field_ref.field_name)
+ self.assertEqual(common_pb2.APPROVAL_TYPE, actual.field_ref.type)
+ self.assertEqual(1, len(actual.approver_refs))
+ self.assertEqual(111, actual.approver_refs[0].user_id)
+ self.assertEqual('What?', actual.survey)
+
+ # Without include_admin_info, some fields are not set.
+ actual = converters.ConvertApprovalDef(
+ approval_def, self.users_by_id, self.config, False)
+ self.assertEqual('LegalApproval', actual.field_ref.field_name)
+ self.assertEqual(common_pb2.APPROVAL_TYPE, actual.field_ref.type)
+ self.assertEqual(0, len(actual.approver_refs))
+ self.assertEqual('', actual.survey)
+
+ def testConvertConfig_Simple(self):
+ """We can convert a simple config to protoc."""
+ actual = converters.ConvertConfig(
+ self.project, self.config, self.users_by_id, {})
+ self.assertEqual('proj', actual.project_name)
+ self.assertEqual(9, len(actual.status_defs))
+ self.assertEqual('New', actual.status_defs[0].status)
+ self.assertEqual(17, len(actual.label_defs))
+ self.assertEqual('Type-Defect', actual.label_defs[0].label)
+ self.assertEqual(
+ ['Type', 'Priority', 'Milestone'], actual.exclusive_label_prefixes)
+ self.assertEqual(0, len(actual.component_defs))
+ self.assertEqual(0, len(actual.field_defs))
+ self.assertEqual(0, len(actual.approval_defs))
+ self.assertEqual(False, actual.restrict_to_known)
+ self.assertEqual(
+ ['Duplicate'], [s.status for s in actual.statuses_offer_merge])
+
+ def testConvertConfig_Normal(self):
+ """We can convert a config with fields and components to protoc."""
+ labels_by_id = {1: 'Security', 2: 'Usability'}
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_3]
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI', label_ids=[2])]
+ self.config.approval_defs.append(tracker_pb2.ApprovalDef(
+ approval_id=3, approver_ids=[111], survey='What?'))
+ self.config.restrict_to_known = True
+ self.config.statuses_offer_merge = ['Duplicate', 'New']
+ actual = converters.ConvertConfig(
+ self.project, self.config, self.users_by_id, labels_by_id)
+ self.assertEqual(1, len(actual.component_defs))
+ self.assertEqual(3, len(actual.field_defs))
+ self.assertEqual(1, len(actual.approval_defs))
+ self.assertEqual('proj', actual.project_name)
+ self.assertEqual(True, actual.restrict_to_known)
+ self.assertEqual(
+ ['Duplicate', 'New'],
+ sorted(s.status for s in actual.statuses_offer_merge))
+
+ def testConvertConfig_FiltersDeletedFieldDefs(self):
+ """Deleted fieldDefs don't make it into the config response."""
+ labels_by_id = {1: 'Security', 2: 'Usability'}
+ deleted_fd1 = tracker_pb2.FieldDef(
+ field_name='DeletedField', field_id=100,
+ field_type=tracker_pb2.FieldTypes.STR_TYPE,
+ applicable_type='',
+ is_deleted=True)
+ deleted_fd2 = tracker_pb2.FieldDef(
+ field_name='RemovedField', field_id=101,
+ field_type=tracker_pb2.FieldTypes.ENUM_TYPE,
+ applicable_type='',
+ is_deleted=True)
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_3, deleted_fd1,
+ deleted_fd2]
+ actual = converters.ConvertConfig(
+ self.project, self.config, self.users_by_id, labels_by_id)
+ self.assertEqual(3, len(actual.field_defs))
+
+ def testConvertProjectTemplateDefs_Normal(self):
+ """We can convert protoc TemplateDefs."""
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path="dude"),
+ ]
+ status_def_1 = tracker_pb2.StatusDef(status='New', means_open=True)
+ status_def_2 = tracker_pb2.StatusDef(status='Old', means_open=False)
+ self.config.well_known_statuses.extend([status_def_1, status_def_2])
+ owner = self.services.user.TestAddUser('owner@example.com', 111)
+ admin1 = self.services.user.TestAddUser('admin1@example.com', 222)
+ admin2 = self.services.user.TestAddUser('admin2@example.com', 333)
+ appr1 = self.services.user.TestAddUser('approver1@example.com', 444)
+ self.config.field_defs = [
+ self.fd_1, # STR_TYPE
+ self.fd_3, # APPROVAl_TYPE
+ self.fd_5, # ENUM_TYPE
+ self.fd_6, # INT_TYPE PHASE
+ self.fd_7, # ENUM_TYPE APPROVAL
+ ]
+ field_values = [
+ tracker_bizobj.MakeFieldValue(
+ self.fd_1.field_id, None, 'honk', None, None, None, False),
+ tracker_bizobj.MakeFieldValue(
+ self.fd_6.field_id, 78, None, None, None, None, False, phase_id=3)]
+ phases = [tracker_pb2.Phase(phase_id=3, name='phaseName')]
+ approval_values = [tracker_pb2.ApprovalValue(
+ approval_id=3, approver_ids=[appr1.user_id], phase_id=3)]
+ labels = ['ApprovalEnum-choice1', 'label-2', 'chicken']
+ templates = [
+ tracker_pb2.TemplateDef(
+ name='Chicken', content='description', summary='summary',
+ summary_must_be_edited=True, owner_id=111, status='New',
+ labels=labels, members_only=True,
+ owner_defaults_to_member=True,
+ admin_ids=[admin1.user_id, admin2.user_id],
+ field_values=field_values, component_ids=[1],
+ component_required=True, phases=phases,
+ approval_values=approval_values),
+ tracker_pb2.TemplateDef(name='Kale')]
+ users_by_id = {
+ owner.user_id: testing_helpers.Blank(
+ display_name=owner.email, email=owner.email, banned=False),
+ admin1.user_id: testing_helpers.Blank(
+ display_name=admin1.email, email=admin1.email, banned=False),
+ admin2.user_id: testing_helpers.Blank(
+ display_name=admin2.email, email=admin2.email, banned=True),
+ appr1.user_id: testing_helpers.Blank(
+ display_name=appr1.email, email=appr1.email, banned=False),
+ }
+ actual = converters.ConvertProjectTemplateDefs(
+ templates, users_by_id, self.config)
+ expected = [
+ project_objects_pb2.TemplateDef(
+ template_name='Chicken',
+ content='description',
+ summary='summary',
+ summary_must_be_edited=True,
+ owner_ref=common_pb2.UserRef(
+ user_id=owner.user_id,
+ display_name=owner.email,
+ is_derived=False),
+ status_ref=common_pb2.StatusRef(
+ status='New',
+ is_derived=False,
+ means_open=True),
+ label_refs=[
+ common_pb2.LabelRef(label='label-2', is_derived=False),
+ common_pb2.LabelRef(label='chicken', is_derived=False)],
+ members_only=True,
+ owner_defaults_to_member=True,
+ admin_refs=[
+ common_pb2.UserRef(
+ user_id=admin1.user_id,
+ display_name=admin1.email,
+ is_derived=False),
+ common_pb2.UserRef(
+ user_id=admin2.user_id,
+ display_name=admin2.email,
+ is_derived=False)],
+ field_values=[
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=self.fd_7.field_id,
+ field_name=self.fd_7.field_name,
+ type=common_pb2.ENUM_TYPE),
+ value='choice1'),
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=self.fd_1.field_id,
+ field_name=self.fd_1.field_name,
+ type=common_pb2.STR_TYPE),
+ value='honk'),
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=self.fd_6.field_id,
+ field_name=self.fd_6.field_name,
+ type=common_pb2.INT_TYPE),
+ value='78',
+ phase_ref=issue_objects_pb2.PhaseRef(
+ phase_name='phaseName'))],
+ component_refs=[
+ common_pb2.ComponentRef(path='dude', is_derived=False)],
+ component_required=True,
+ phases=[issue_objects_pb2.PhaseDef(
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='phaseName'))],
+ approval_values=[
+ issue_objects_pb2.Approval(
+ field_ref=common_pb2.FieldRef(
+ field_id=self.fd_3.field_id,
+ field_name=self.fd_3.field_name,
+ type=common_pb2.APPROVAL_TYPE),
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='phaseName'),
+ approver_refs=[common_pb2.UserRef(
+ user_id=appr1.user_id,
+ display_name=appr1.email,
+ is_derived=False)])],
+ ),
+ project_objects_pb2.TemplateDef(
+ template_name='Kale',
+ status_ref=common_pb2.StatusRef(
+ status='----',
+ means_open=True),
+ owner_defaults_to_member=True)]
+ self.assertEqual(actual, expected)
+
+ def testConvertTemplateDefs_Empty(self):
+ """We can convert an empty list of protoc TemplateDefs."""
+ actual = converters.ConvertProjectTemplateDefs([], {}, self.config)
+ self.assertEqual(actual, [])
+
+ def testConvertHotlist(self):
+ """We can convert a hotlist to protoc."""
+ hotlist = fake.Hotlist(
+ 'Fake-hotlist', 123, is_private=True,
+ owner_ids=[self.user_1.user_id], editor_ids=[self.user_2.user_id],
+ follower_ids=[self.user_3.user_id])
+ hotlist.summary = 'A fake hotlist.'
+ hotlist.description = 'Detailed description of the fake hotlist.'
+ hotlist.default_col_spec = 'cows tho'
+ actual = converters.ConvertHotlist(hotlist, self.users_by_id)
+ self.assertEqual(actual,
+ features_objects_pb2.Hotlist(
+ name=hotlist.name,
+ summary=hotlist.summary,
+ description=hotlist.description,
+ default_col_spec=hotlist.default_col_spec,
+ is_private=hotlist.is_private,
+ owner_ref=common_pb2.UserRef(
+ display_name=self.user_1.email,
+ user_id=self.user_1.user_id),
+ editor_refs=[common_pb2.UserRef(
+ display_name=self.user_2.email,
+ user_id=self.user_2.user_id)],
+ follower_refs=[common_pb2.UserRef(
+ display_name=testing_helpers.ObscuredEmail(
+ self.user_3.email),
+ user_id=self.user_3.user_id)]))
+
+
+ def testConvertHotlistItem(self):
+ """We can convert a HotlistItem to protoc."""
+ project_2 = self.services.project.TestAddProject(
+ 'proj2', project_id=788)
+ config_2 = tracker_bizobj.MakeDefaultProjectIssueConfig(
+ project_2.project_id)
+ config_2.field_defs = [self.fd_2]
+ self.config.field_defs = [self.fd_1]
+
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[])
+ self.services.features.UpdateHotlistItems(
+ self.cnxn, hotlist.hotlist_id, [],
+ [(self.issue_1.issue_id, 222, 12345, 'Note')])
+ issues_by_id = {self.issue_1.issue_id: self.issue_1}
+ related_refs = {}
+ harmonized_config = tracker_bizobj.HarmonizeConfigs([self.config, config_2])
+
+ actual = converters.ConvertHotlistItems(
+ hotlist.items, issues_by_id, self.users_by_id, related_refs,
+ harmonized_config)
+
+ expected_issue = converters.ConvertIssue(
+ self.issue_1, self.users_by_id, related_refs, harmonized_config)
+ self.assertEqual(
+ [features_objects_pb2.HotlistItem(
+ issue=expected_issue,
+ rank=1,
+ adder_ref=common_pb2.UserRef(
+ user_id=222,
+ display_name='two@example.com'),
+ added_timestamp=12345,
+ note='Note')],
+ actual)
+
+ def testConvertValueAndWhy(self):
+ """We can covert a dict wth 'why' and 'value' fields to a ValueAndWhy PB."""
+ actual = converters.ConvertValueAndWhy({'value': 'Foo', 'why': 'Because'})
+ self.assertEqual(
+ common_pb2.ValueAndWhy(value='Foo', why='Because'),
+ actual)
+
+ def testConvertValueAndWhyList(self):
+ """We can convert a list of value and why dicts."""
+ actual = converters.ConvertValueAndWhyList([
+ {'value': 'A', 'why': 'Because A'},
+ {'value': 'B'},
+ {'why': 'Why what?'},
+ {}])
+ self.assertEqual(
+ [common_pb2.ValueAndWhy(value='A', why='Because A'),
+ common_pb2.ValueAndWhy(value='B'),
+ common_pb2.ValueAndWhy(why='Why what?'),
+ common_pb2.ValueAndWhy()],
+ actual)
+
+ def testRedistributeEnumFieldsIntoLabels(self):
+ # function called and tests covered by
+ # IngestIssueDelta and IngestApprovalDelta
+ pass
diff --git a/api/test/features_servicer_test.py b/api/test/features_servicer_test.py
new file mode 100644
index 0000000..7a7180b
--- /dev/null
+++ b/api/test/features_servicer_test.py
@@ -0,0 +1,1039 @@
+# Copyright 2018 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 projects servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import json
+import mock
+import unittest
+import mox
+
+from google.protobuf import wrappers_pb2
+
+from components.prpc import codes
+from components.prpc import context
+from components.prpc import server
+
+from api import converters
+from api.api_proto import common_pb2
+from api.api_proto import features_pb2
+from api.api_proto import features_objects_pb2
+from api.api_proto import issue_objects_pb2
+from framework import authdata
+from framework import exceptions
+from framework import monorailcontext
+from framework import permissions
+from framework import sorting
+from testing import fake
+from testing import testing_helpers
+from tracker import tracker_bizobj
+from services import features_svc
+from services import service_manager
+
+# Import component_helpers_test to mock cloudstorage before it is imported by
+# component_helpers via features servicer.
+from features.test import component_helpers_test
+from api import features_servicer # pylint: disable=ungrouped-imports
+
+
+class FeaturesServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.mox = mox.Mox()
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ cache_manager=fake.CacheManager(),
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ features=fake.FeaturesService(),
+ hotlist_star=fake.HotlistStarService())
+ sorting.InitializeArtValues(self.services)
+
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=789, owner_ids=[111], contrib_ids=[222, 333])
+ self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ self.user1 = self.services.user.TestAddUser('owner@example.com', 111)
+ self.user2 = self.services.user.TestAddUser('editor@example.com', 222)
+ self.user3 = self.services.user.TestAddUser('foo@example.com', 333)
+ self.user4 = self.services.user.TestAddUser('bar@example.com', 444)
+ self.features_svcr = features_servicer.FeaturesServicer(
+ self.services, make_rate_limiter=False)
+ self.prpc_context = context.ServicerContext()
+ self.prpc_context.set_code(codes.StatusCode.OK)
+ self.issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=78901)
+ self.issue_2 = fake.MakeTestIssue(
+ 789, 2, 'sum', 'Fixed', 111, project_name='proj', issue_id=78902,
+ closed_timestamp=112223344)
+ self.issue_3 = fake.MakeTestIssue(
+ 789, 3, 'sum', 'New', 111, project_name='proj', issue_id=78903)
+
+ self.services.issue.TestAddIssue(self.issue_1)
+ self.services.issue.TestAddIssue(self.issue_2)
+ self.services.issue.TestAddIssue(self.issue_3)
+
+ self.project_2 = self.services.project.TestAddProject(
+ 'proj2', project_id=788, owner_ids=[111], contrib_ids=[222, 333])
+ self.config_2 = tracker_bizobj.MakeDefaultProjectIssueConfig(788)
+ self.issue_21 = fake.MakeTestIssue(
+ 788, 1, 'sum', 'New', 111, project_name='proj2', issue_id=78801)
+ self.issue_22 = fake.MakeTestIssue(
+ 788, 2, 'sum', 'New', 111, project_name='proj2', issue_id=78802)
+ self.issue_23 = fake.MakeTestIssue(
+ 788, 3, 'sum', 'New', 111, project_name='proj2', issue_id=78803)
+ self.services.issue.TestAddIssue(self.issue_21)
+ self.services.issue.TestAddIssue(self.issue_22)
+ self.services.issue.TestAddIssue(self.issue_23)
+
+ self.PAST_TIME = 123456
+
+ # For testing PredictComponent
+ self._ml_engine = component_helpers_test.FakeMLEngine(self)
+ self._top_words = None
+ self._components_by_index = None
+
+ mock.patch(
+ 'services.ml_helpers.setup_ml_engine', lambda: self._ml_engine).start()
+ mock.patch(
+ 'features.component_helpers._GetTopWords',
+ lambda _: self._top_words).start()
+ mock.patch('cloudstorage.open', self.cloudstorageOpen).start()
+ mock.patch('settings.component_features', 5).start()
+
+ self.addCleanup(mock.patch.stopall)
+
+ def cloudstorageOpen(self, name, mode):
+ """Create a file mock that returns self._components_by_index when read."""
+ open_fn = mock.mock_open(read_data=json.dumps(self._components_by_index))
+ return open_fn(name, mode)
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def CallWrapped(self, wrapped_handler, *args, **kwargs):
+ return wrapped_handler.wrapped(self.features_svcr, *args, **kwargs)
+
+ def testListHotlistsByUser_SearchByEmail(self):
+ """We can get a list of hotlists for a given email."""
+ # Public hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(display_name='owner@example.com')
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're not authenticated
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual(111, hotlist.owner_ref.user_id)
+ self.assertEqual('ow...@example.com', hotlist.owner_ref.display_name)
+ self.assertEqual('Fake-Hotlist', hotlist.name)
+ self.assertEqual('Summary', hotlist.summary)
+ self.assertEqual('Description', hotlist.description)
+
+ def testListHotlistsByUser_SearchByOwner(self):
+ """We can get a list of hotlists for a given user."""
+ # Public hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(user_id=111)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual(111, hotlist.owner_ref.user_id)
+ # User1 and user3 share self.project.
+ self.assertEqual('owner@example.com', hotlist.owner_ref.display_name)
+ self.assertEqual('Fake-Hotlist', hotlist.name)
+ self.assertEqual('Summary', hotlist.summary)
+ self.assertEqual('Description', hotlist.description)
+
+ def testListHotlistsByUser_SearchByEditor(self):
+ """We can get a list of hotlists for a given user."""
+ # Public hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ # Query for issues for 'editor@example.com'
+ user_ref = common_pb2.UserRef(user_id=222)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual(111, hotlist.owner_ref.user_id)
+ # User1 and user3 share self.project.
+ self.assertEqual('owner@example.com', hotlist.owner_ref.display_name)
+ self.assertEqual('Fake-Hotlist', hotlist.name)
+ self.assertEqual('Summary', hotlist.summary)
+ self.assertEqual('Description', hotlist.description)
+
+ def testListHotlistsByUser_NotSignedIn(self):
+ # Public hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(user_id=111)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're not authenticated
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual(111, hotlist.owner_ref.user_id)
+
+ def testListHotlistsByUser_Empty(self):
+ """There are no hotlists for the given user."""
+ # Public hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ # Query for issues for 'bar@example.com'
+ user_ref = common_pb2.UserRef(user_id=444)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+
+ self.assertEqual(0, len(response.hotlists))
+
+ def testListHotlistsByUser_NoHotlists(self):
+ """There are no hotlists."""
+ # No hotlists
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(user_id=111)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+ self.assertEqual(0, len(response.hotlists))
+
+ def testListHotlistsByUser_PrivateIssueAsOwner(self):
+ # Private hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222], is_private=True)
+
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(user_id=111)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're authenticated as 'owner@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual(111, hotlist.owner_ref.user_id)
+
+ def testListHotlistsByUser_PrivateIssueAsEditor(self):
+ # Private hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222], is_private=True)
+
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(user_id=111)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're authenticated as 'editor@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='editor@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual(111, hotlist.owner_ref.user_id)
+
+ def testListHotlistsByUser_PrivateIssueNoAccess(self):
+ # Private hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222], is_private=True)
+
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(user_id=111)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+
+ self.assertEqual(0, len(response.hotlists))
+
+ def testListHotlistsByUser_PrivateIssueNotSignedIn(self):
+ # Private hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222], is_private=True)
+
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(user_id=111)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're not authenticated
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+
+ self.assertEqual(0, len(response.hotlists))
+
+ def AddIssueToHotlist(self, hotlist_id, issue_id=78901, adder_id=111):
+ self.services.features.AddIssuesToHotlists(
+ self.cnxn, [hotlist_id], [(issue_id, adder_id, 0, '')],
+ None, None, None)
+
+ def testListHotlistsByIssue_Normal(self):
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.AddIssueToHotlist(hotlist.hotlist_id)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ request = features_pb2.ListHotlistsByIssueRequest(issue=issue_ref)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByIssue, mc,
+ request)
+
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual('Fake-Hotlist', hotlist.name)
+
+ def testListHotlistsByIssue_NotSignedIn(self):
+ # Public hostlist owned by 'owner@example.com'
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.AddIssueToHotlist(hotlist.hotlist_id)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ request = features_pb2.ListHotlistsByIssueRequest(issue=issue_ref)
+
+ # We're not authenticated
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByIssue, mc,
+ request)
+
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual('Fake-Hotlist', hotlist.name)
+
+ def testListHotlistsByIssue_Empty(self):
+ """There are no hotlists with the given issue."""
+ # Public hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ request = features_pb2.ListHotlistsByIssueRequest(issue=issue_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByIssue, mc,
+ request)
+
+ self.assertEqual(0, len(response.hotlists))
+
+ def testListHotlistsByIssue_NoHotlists(self):
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ request = features_pb2.ListHotlistsByIssueRequest(issue=issue_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByIssue, mc,
+ request)
+ self.assertEqual(0, len(response.hotlists))
+
+ def testListHotlistsByIssue_PrivateHotlistAsOwner(self):
+ """An owner can view their private issues."""
+ # Private hostlist owned by 'owner@example.com'
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222], is_private=True)
+ self.AddIssueToHotlist(hotlist.hotlist_id)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ request = features_pb2.ListHotlistsByIssueRequest(issue=issue_ref)
+
+ # We're authenticated as 'owner@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByIssue, mc,
+ request)
+
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual('Fake-Hotlist', hotlist.name)
+
+ def testListHotlistsByIssue_PrivateHotlistNoAccess(self):
+ # Private hostlist owned by 'owner@example.com'
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222], is_private=True)
+ self.AddIssueToHotlist(hotlist.hotlist_id)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ request = features_pb2.ListHotlistsByIssueRequest(issue=issue_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByIssue, mc,
+ request)
+
+ self.assertEqual(0, len(response.hotlists))
+
+ def testListHotlistsByIssue_NonProjectHotlists(self):
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn,
+ 'Fake-Hotlist',
+ 'Summary',
+ 'Description',
+ owner_ids=[111],
+ editor_ids=[222])
+ spam_hotlist = self.services.features.CreateHotlist(
+ self.cnxn,
+ 'Spam-Hotlist',
+ 'Summary',
+ 'Description',
+ owner_ids=[444],
+ editor_ids=[])
+ another_hotlist = self.services.features.CreateHotlist(
+ self.cnxn,
+ 'Another-Hotlist',
+ 'Summary',
+ 'Description',
+ owner_ids=[111],
+ editor_ids=[])
+ self.AddIssueToHotlist(hotlist.hotlist_id)
+ self.AddIssueToHotlist(spam_hotlist.hotlist_id)
+ self.AddIssueToHotlist(another_hotlist.hotlist_id)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ request = features_pb2.ListHotlistsByIssueRequest(issue=issue_ref)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(
+ self.features_svcr.ListHotlistsByIssue, mc, request)
+
+ self.assertEqual(2, len(response.hotlists))
+ self.assertEqual('Fake-Hotlist', response.hotlists[0].name)
+ self.assertEqual('Another-Hotlist', response.hotlists[1].name)
+
+ def testListRecentlyVisitedHotlists(self):
+ hotlists = [
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[self.user2.user_id], editor_ids=[self.user1.user_id],
+ default_col_spec='chicken'),
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist-2', 'Summary', 'Description',
+ owner_ids=[self.user1.user_id], editor_ids=[self.user2.user_id],
+ default_col_spec='honk'),
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Private-Hotlist', 'Summary', 'Description',
+ owner_ids=[self.user3.user_id], editor_ids=[self.user2.user_id],
+ is_private=True)]
+
+ for hotlist in hotlists:
+ self.services.user.AddVisitedHotlist(
+ self.cnxn, self.user1.user_id, hotlist.hotlist_id)
+
+ request = features_pb2.ListRecentlyVisitedHotlistsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user1.email)
+ response = self.CallWrapped(
+ self.features_svcr.ListRecentlyVisitedHotlists, mc, request)
+
+ expected_hotlists = [
+ features_objects_pb2.Hotlist(
+ owner_ref=common_pb2.UserRef(
+ user_id=self.user2.user_id, display_name=self.user2.email),
+ editor_refs=[
+ common_pb2.UserRef(
+ user_id=self.user1.user_id, display_name=self.user1.email)
+ ],
+ name='Fake-Hotlist',
+ summary='Summary',
+ description='Description',
+ is_private=False,
+ default_col_spec='chicken'),
+ features_objects_pb2.Hotlist(
+ owner_ref=common_pb2.UserRef(
+ user_id=self.user1.user_id, display_name=self.user1.email),
+ editor_refs=[
+ common_pb2.UserRef(
+ user_id=self.user2.user_id, display_name=self.user2.email)
+ ],
+ name='Fake-Hotlist-2',
+ summary='Summary',
+ description='Description',
+ is_private=False,
+ default_col_spec='honk')
+ ]
+
+ # We don't have permission to see the last issue, because it is marked as
+ # private and we're not owners or editors.
+ self.assertEqual(expected_hotlists, list(response.hotlists))
+
+ def testListRecentlyVisitedHotlists_Anon(self):
+ request = features_pb2.ListRecentlyVisitedHotlistsRequest()
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ response = self.CallWrapped(
+ self.features_svcr.ListRecentlyVisitedHotlists, mc, request)
+ self.assertEqual(0, len(response.hotlists))
+
+ def testListStarredHotlists(self):
+ hotlists = [
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[self.user2.user_id], editor_ids=[self.user1.user_id],
+ default_col_spec='cow chicken'),
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist-2', 'Summary', 'Description',
+ owner_ids=[self.user1.user_id],
+ editor_ids=[self.user2.user_id, self.user3.user_id],
+ default_col_spec=''),
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Private-Hotlist', 'Summary', 'Description',
+ owner_ids=[self.user3.user_id], editor_ids=[self.user2.user_id],
+ is_private=True, default_col_spec='chicken')]
+
+ for hotlist in hotlists:
+ self.services.hotlist_star.SetStar(
+ self.cnxn, hotlist.hotlist_id, self.user1.user_id, True)
+
+ request = features_pb2.ListStarredHotlistsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.features_svcr.ListStarredHotlists, mc, request)
+
+ expected_hotlists = [
+ features_objects_pb2.Hotlist(
+ owner_ref=common_pb2.UserRef(
+ user_id=self.user2.user_id, display_name=self.user2.email),
+ editor_refs=[
+ common_pb2.UserRef(
+ user_id=self.user1.user_id, display_name=self.user1.email)
+ ],
+ name='Fake-Hotlist',
+ summary='Summary',
+ description='Description',
+ is_private=False,
+ default_col_spec='cow chicken'),
+ features_objects_pb2.Hotlist(
+ owner_ref=common_pb2.UserRef(
+ user_id=self.user1.user_id, display_name=self.user1.email),
+ editor_refs=[
+ common_pb2.UserRef(
+ user_id=self.user2.user_id, display_name=self.user2.email),
+ common_pb2.UserRef(
+ user_id=self.user3.user_id, display_name=self.user3.email)
+ ],
+ name='Fake-Hotlist-2',
+ summary='Summary',
+ description='Description',
+ is_private=False)
+ ]
+
+ # We don't have permission to see the last issue, because it is marked as
+ # private and we're not owners or editors.
+ self.assertEqual(expected_hotlists, list(response.hotlists))
+
+ def testListStarredHotlists_Anon(self):
+ request = features_pb2.ListStarredHotlistsRequest()
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ response = self.CallWrapped(
+ self.features_svcr.ListStarredHotlists, mc, request)
+ self.assertEqual(0, len(response.hotlists))
+
+ def CallGetStarCount(self):
+ # Query for hotlists for 'owner@example.com'
+ owner_ref = common_pb2.UserRef(user_id=111)
+ hotlist_ref = common_pb2.HotlistRef(name='Fake-Hotlist', owner=owner_ref)
+ request = features_pb2.GetHotlistStarCountRequest(hotlist_ref=hotlist_ref)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.features_svcr.GetHotlistStarCount, mc, request)
+ return response.star_count
+
+ def CallStar(self, requester='owner@example.com', starred=True):
+ # Query for hotlists for 'owner@example.com'
+ owner_ref = common_pb2.UserRef(user_id=111)
+ hotlist_ref = common_pb2.HotlistRef(name='Fake-Hotlist', owner=owner_ref)
+ request = features_pb2.StarHotlistRequest(
+ hotlist_ref=hotlist_ref, starred=starred)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=requester)
+ response = self.CallWrapped(
+ self.features_svcr.StarHotlist, mc, request)
+ return response.star_count
+
+ def testStarCount_Normal(self):
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.assertEqual(0, self.CallGetStarCount())
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ def testStarCount_StarTwiceSameUser(self):
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ def testStarCount_StarTwiceDifferentUser(self):
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(2, self.CallStar(requester='user_222@example.com'))
+ self.assertEqual(2, self.CallGetStarCount())
+
+ def testStarCount_RemoveStarTwiceSameUser(self):
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ self.assertEqual(0, self.CallStar(starred=False))
+ self.assertEqual(0, self.CallStar(starred=False))
+ self.assertEqual(0, self.CallGetStarCount())
+
+ def testStarCount_RemoveStarTwiceDifferentUser(self):
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(2, self.CallStar(requester='user_222@example.com'))
+ self.assertEqual(2, self.CallGetStarCount())
+
+ self.assertEqual(1, self.CallStar(starred=False))
+ self.assertEqual(
+ 0, self.CallStar(requester='user_222@example.com', starred=False))
+ self.assertEqual(0, self.CallGetStarCount())
+
+ def testGetHotlist(self):
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[self.user3.user_id], editor_ids=[self.user4.user_id],
+ is_private=True, default_col_spec='corgi butts')
+
+ owner_ref = common_pb2.UserRef(user_id=self.user3.user_id)
+ hotlist_ref = common_pb2.HotlistRef(
+ name=hotlist.name, owner=owner_ref)
+ request = features_pb2.GetHotlistRequest(hotlist_ref=hotlist_ref)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user4.email)
+ mc.LookupLoggedInUserPerms(None)
+ response = self.CallWrapped(
+ self.features_svcr.GetHotlist, mc, request)
+
+ self.assertEqual(
+ response.hotlist,
+ features_objects_pb2.Hotlist(
+ owner_ref=common_pb2.UserRef(
+ user_id=self.user3.user_id,
+ display_name=testing_helpers.ObscuredEmail(self.user3.email)),
+ editor_refs=[common_pb2.UserRef(
+ user_id=self.user4.user_id,
+ display_name=self.user4.email)],
+ name=hotlist.name,
+ summary=hotlist.summary,
+ description=hotlist.description,
+ default_col_spec='corgi butts',
+ is_private=True))
+
+ def testGetHotlist_BadInput(self):
+ hotlist_ref = common_pb2.HotlistRef()
+ request = features_pb2.GetHotlistRequest(hotlist_ref=hotlist_ref)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ with self.assertRaises(features_svc.NoSuchHotlistException):
+ self.CallWrapped(self.features_svcr.GetHotlist, mc, request)
+
+ def testCreateHotlist_Normal(self):
+ request = features_pb2.CreateHotlistRequest(
+ name='Fake-Hotlist',
+ summary='Summary',
+ description='Description',
+ editor_refs=[
+ common_pb2.UserRef(user_id=222),
+ common_pb2.UserRef(display_name='foo@example.com')],
+ issue_refs=[
+ common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)],
+ is_private=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.features_svcr.CreateHotlist, mc, request)
+
+ # Check that the hotlist was successfuly added.
+ hotlist_id = self.services.features.LookupHotlistIDs(
+ self.cnxn, ['Fake-Hotlist'], [111]).get(('fake-hotlist', 111))
+ hotlist = self.services.features.GetHotlist(self.cnxn, hotlist_id)
+ self.assertEqual('Summary', hotlist.summary)
+ self.assertEqual('Description', hotlist.description)
+ self.assertEqual([111], hotlist.owner_ids)
+ self.assertEqual([222, 333], hotlist.editor_ids)
+ self.assertEqual(
+ [self.issue_1.issue_id, self.issue_2.issue_id],
+ [item.issue_id for item in hotlist.items])
+ self.assertTrue(hotlist.is_private)
+
+ def testCreateHotlist_Simple(self):
+ request = features_pb2.CreateHotlistRequest(
+ name='Fake-Hotlist',
+ summary='Summary',
+ description='Description')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.features_svcr.CreateHotlist, mc, request)
+
+ # Check that the hotlist was successfuly added.
+ hotlist_id = self.services.features.LookupHotlistIDs(
+ self.cnxn, ['Fake-Hotlist'], [111]).get(('fake-hotlist', 111))
+ hotlist = self.services.features.GetHotlist(self.cnxn, hotlist_id)
+ self.assertEqual('Summary', hotlist.summary)
+ self.assertEqual('Description', hotlist.description)
+ self.assertEqual([111], hotlist.owner_ids)
+ self.assertEqual([], hotlist.editor_ids)
+ self.assertEqual(0, len(hotlist.items))
+ self.assertFalse(hotlist.is_private)
+
+ def testCheckHotlistName_OK(self):
+ request = features_pb2.CheckHotlistNameRequest(name='Fake-Hotlist')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ result = self.CallWrapped(self.features_svcr.CheckHotlistName, mc, request)
+ self.assertEqual('', result.error)
+
+ def testCheckHotlistName_Anon(self):
+ request = features_pb2.CheckHotlistNameRequest(name='Fake-Hotlist')
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.features_svcr.CheckHotlistName, mc, request)
+
+ def testCheckHotlistName_InvalidName(self):
+ request = features_pb2.CheckHotlistNameRequest(name='**Invalid**')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ result = self.CallWrapped(self.features_svcr.CheckHotlistName, mc, request)
+ self.assertNotEqual('', result.error)
+
+ def testCheckHotlistName_AlreadyExists(self):
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[])
+
+ request = features_pb2.CheckHotlistNameRequest(name='Fake-Hotlist')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ result = self.CallWrapped(self.features_svcr.CheckHotlistName, mc, request)
+ self.assertNotEqual('', result.error)
+
+ def testRemoveIssuesFromHotlists(self):
+ # Create two hotlists with issues 1 and 2.
+ hotlist_1 = self.services.features.CreateHotlist(
+ self.cnxn, 'Hotlist-1', 'Summary', 'Description', owner_ids=[111],
+ editor_ids=[])
+ hotlist_2 = self.services.features.CreateHotlist(
+ self.cnxn, 'Hotlist-2', 'Summary', 'Description', owner_ids=[111],
+ editor_ids=[])
+ self.services.features.AddIssuesToHotlists(
+ self.cnxn,
+ [hotlist_1.hotlist_id, hotlist_2.hotlist_id],
+ [(self.issue_1.issue_id, 111, 0, ''),
+ (self.issue_2.issue_id, 111, 0, '')],
+ None, None, None)
+
+ # Remove Issue 1 from both hotlists.
+ request = features_pb2.RemoveIssuesFromHotlistsRequest(
+ hotlist_refs=[
+ common_pb2.HotlistRef(
+ name='Hotlist-1',
+ owner=common_pb2.UserRef(user_id=111)),
+ common_pb2.HotlistRef(
+ name='Hotlist-2',
+ owner=common_pb2.UserRef(user_id=111))],
+ issue_refs=[
+ common_pb2.IssueRef(project_name='proj', local_id=1)])
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.features_svcr.RemoveIssuesFromHotlists, mc, request)
+
+ # Only Issue 2 should remain in both lists.
+ self.assertEqual(
+ [self.issue_2.issue_id],
+ [item.issue_id for item in hotlist_1.items])
+ self.assertEqual(
+ [self.issue_2.issue_id],
+ [item.issue_id for item in hotlist_2.items])
+
+ def testAddIssuesToHotlists(self):
+ # Create two hotlists
+ hotlist_1 = self.services.features.CreateHotlist(
+ self.cnxn, 'Hotlist-1', 'Summary', 'Description', owner_ids=[111],
+ editor_ids=[])
+ hotlist_2 = self.services.features.CreateHotlist(
+ self.cnxn, 'Hotlist-2', 'Summary', 'Description', owner_ids=[111],
+ editor_ids=[])
+
+ # Add Issue 1 to both hotlists
+ request = features_pb2.AddIssuesToHotlistsRequest(
+ note='Foo',
+ hotlist_refs=[
+ common_pb2.HotlistRef(
+ name='Hotlist-1',
+ owner=common_pb2.UserRef(user_id=111)),
+ common_pb2.HotlistRef(
+ name='Hotlist-2',
+ owner=common_pb2.UserRef(user_id=111))],
+ issue_refs=[
+ common_pb2.IssueRef(project_name='proj', local_id=1)])
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.features_svcr.AddIssuesToHotlists, mc, request)
+
+ self.assertEqual(
+ [self.issue_1.issue_id],
+ [item.issue_id for item in hotlist_1.items])
+ self.assertEqual(
+ [self.issue_1.issue_id],
+ [item.issue_id for item in hotlist_2.items])
+
+ self.assertEqual('Foo', hotlist_1.items[0].note)
+ self.assertEqual('Foo', hotlist_2.items[0].note)
+
+ def testRerankHotlistIssues(self):
+ """Rerank a hotlist."""
+ issue_3 = fake.MakeTestIssue(
+ 789, 3, 'sum', 'New', 111, project_name='proj', issue_id=78903)
+ issue_4 = fake.MakeTestIssue(
+ 789, 4, 'sum', 'New', 111, project_name='proj', issue_id=78904)
+ self.services.issue.TestAddIssue(issue_3)
+ self.services.issue.TestAddIssue(issue_4)
+
+ owner_ids = [self.user1.user_id]
+ follower_ids = [self.user2.user_id]
+ editor_ids = [self.user3.user_id]
+ hotlist_items = [
+ (78904, 31, self.user2.user_id, self.PAST_TIME, 'note'),
+ (78903, 21, self.user2.user_id, self.PAST_TIME, 'note'),
+ (78902, 11, self.user2.user_id, self.PAST_TIME, 'note'),
+ (78901, 1, self.user2.user_id, self.PAST_TIME, 'note')]
+ hotlist = self.services.features.TestAddHotlist(
+ 'RerankHotlistName', summary='summary', owner_ids=owner_ids,
+ editor_ids=editor_ids, follower_ids=follower_ids,
+ hotlist_id=1236, hotlist_item_fields=hotlist_items)
+
+ request = features_pb2.RerankHotlistIssuesRequest(
+ hotlist_ref=common_pb2.HotlistRef(
+ name='RerankHotlistName',
+ owner=common_pb2.UserRef(user_id=self.user1.user_id)),
+ moved_refs=[common_pb2.IssueRef(
+ project_name='proj', local_id=2)],
+ target_ref=common_pb2.IssueRef(project_name='proj', local_id=4),
+ split_above=True)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user1.email)
+ mc.LookupLoggedInUserPerms(self.project)
+ self.CallWrapped(self.features_svcr.RerankHotlistIssues, mc, request)
+
+ self.assertEqual(
+ [item.issue_id for item in hotlist.items],
+ [78901, 78903, 78902, 78904])
+
+ def testUpdateHotlistIssueNote(self):
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Hotlist-1', 'Summary', 'Description', owner_ids=[111],
+ editor_ids=[])
+ self.services.features.AddIssuesToHotlists(
+ self.cnxn,
+ [hotlist.hotlist_id], [(self.issue_1.issue_id, 111, 0, '')],
+ None, None, None)
+
+ request = features_pb2.UpdateHotlistIssueNoteRequest(
+ hotlist_ref=common_pb2.HotlistRef(
+ name='Hotlist-1',
+ owner=common_pb2.UserRef(user_id=111)),
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ note='Note')
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.features_svcr.UpdateHotlistIssueNote, mc, request)
+
+ self.assertEqual('Note', hotlist.items[0].note)
+
+ def testUpdateHotlistIssueNote_NotAllowed(self):
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Hotlist-1', 'Summary', 'Description', owner_ids=[222],
+ editor_ids=[])
+ self.services.features.AddIssuesToHotlists(
+ self.cnxn,
+ [hotlist.hotlist_id], [(self.issue_1.issue_id, 222, 0, '')],
+ None, None, None)
+
+ request = features_pb2.UpdateHotlistIssueNoteRequest(
+ hotlist_ref=common_pb2.HotlistRef(
+ name='Hotlist-1',
+ owner=common_pb2.UserRef(user_id=222)),
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ note='Note')
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.features_svcr.UpdateHotlistIssueNote, mc, request)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user2.email)
+ mc.LookupLoggedInUserPerms(None)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user2.email)
+ mc.LookupLoggedInUserPerms(None)
+
+ def testDeleteHotlist(self):
+ """Test we can delete a hotlist via the API."""
+ owner_ids = [self.user2.user_id]
+ editor_ids = []
+ hotlist = self.services.features.TestAddHotlist(
+ name='Hotlist-1', summary='summary', description='description',
+ owner_ids=owner_ids, editor_ids=editor_ids, hotlist_id=1235)
+ request = features_pb2.DeleteHotlistRequest(
+ hotlist_ref=common_pb2.HotlistRef(
+ name=hotlist.name,
+ owner=common_pb2.UserRef(user_id=self.user2.user_id)))
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user2.email)
+ mc.LookupLoggedInUserPerms(None)
+ self.CallWrapped(self.features_svcr.DeleteHotlist, mc, request)
+
+ self.assertTrue(
+ hotlist.hotlist_id in self.services.features.expunged_hotlist_ids)
+
+ def testPredictComponent_Normal(self):
+ """Test normal case when predicted component exists."""
+ component_id = self.services.config.CreateComponentDef(
+ cnxn=None, project_id=self.project.project_id, path='Ruta>Baga',
+ docstring='', deprecated=False, admin_ids=[], cc_ids=[], created=None,
+ creator_id=None, label_ids=[])
+
+ self._top_words = {
+ 'foo': 0,
+ 'bar': 1,
+ 'baz': 2}
+ self._components_by_index = {
+ '0': '123',
+ '1': str(component_id),
+ '2': '789'}
+ self._ml_engine.expected_features = [3, 0, 1, 0, 0]
+ self._ml_engine.scores = [5, 10, 3]
+
+ request = features_pb2.PredictComponentRequest(
+ project_name='proj',
+ text='foo baz foo foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ result = self.CallWrapped(self.features_svcr.PredictComponent, mc, request)
+
+ self.assertEqual(
+ common_pb2.ComponentRef(
+ path='Ruta>Baga'),
+ result.component_ref)
+
+ def testPredictComponent_NoPrediction(self):
+ """Test case when no component id was predicted."""
+ self._top_words = {
+ 'foo': 0,
+ 'bar': 1,
+ 'baz': 2}
+ self._components_by_index = {
+ '0': '123',
+ '1': '456',
+ '2': '789'}
+ self._ml_engine.expected_features = [3, 0, 1, 0, 0]
+ self._ml_engine.scores = [5, 10, 3]
+
+ request = features_pb2.PredictComponentRequest(
+ project_name='proj',
+ text='foo baz foo foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ result = self.CallWrapped(self.features_svcr.PredictComponent, mc, request)
+
+ self.assertEqual(common_pb2.ComponentRef(), result.component_ref)
diff --git a/api/test/issues_servicer_test.py b/api/test/issues_servicer_test.py
new file mode 100644
index 0000000..2c46f7c
--- /dev/null
+++ b/api/test/issues_servicer_test.py
@@ -0,0 +1,2693 @@
+# Copyright 2018 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 logging
+import sys
+import time
+import unittest
+from mock import ANY, Mock, patch
+
+from google.protobuf import empty_pb2
+
+from components.prpc import codes
+from components.prpc import context
+from components.prpc import server
+
+from api import issues_servicer
+from api import converters
+from api.api_proto import common_pb2
+from api.api_proto import issues_pb2
+from api.api_proto import issue_objects_pb2
+from api.api_proto import common_pb2
+from businesslogic import work_env
+from features import filterrules_helpers
+from features import send_notifications
+from framework import authdata
+from framework import exceptions
+from framework import framework_views
+from framework import monorailcontext
+from framework import permissions
+from search import frontendsearchpipeline
+from proto import tracker_pb2
+from proto import project_pb2
+from testing import fake
+from tracker import tracker_bizobj
+from services import service_manager
+from proto import tracker_pb2
+
+
+class IssuesServicerTest(unittest.TestCase):
+
+ NOW = 1234567890
+
+ def setUp(self):
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ features=fake.FeaturesService(),
+ issue=fake.IssueService(),
+ issue_star=fake.IssueStarService(),
+ project=fake.ProjectService(),
+ spam=fake.SpamService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=789, owner_ids=[111], contrib_ids=[222, 333])
+ self.user_1 = self.services.user.TestAddUser('owner@example.com', 111)
+ self.user_2 = self.services.user.TestAddUser('approver2@example.com', 222)
+ self.user_3 = self.services.user.TestAddUser('approver3@example.com', 333)
+ self.user_4 = self.services.user.TestAddUser('nonmember@example.com', 444)
+ self.issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj',
+ opened_timestamp=self.NOW, issue_id=1001)
+ self.issue_2 = fake.MakeTestIssue(
+ 789, 2, 'sum', 'New', 111, project_name='proj', issue_id=1002)
+ self.issue_1.blocked_on_iids.append(self.issue_2.issue_id)
+ self.issue_1.blocked_on_ranks.append(sys.maxint)
+ self.services.issue.TestAddIssue(self.issue_1)
+ self.services.issue.TestAddIssue(self.issue_2)
+ self.issues_svcr = issues_servicer.IssuesServicer(
+ self.services, make_rate_limiter=False)
+ self.prpc_context = context.ServicerContext()
+ self.prpc_context.set_code(server.StatusCode.OK)
+ self.auth = authdata.AuthData(user_id=333, email='approver3@example.com')
+
+ self.fd_1 = tracker_pb2.FieldDef(
+ field_name='FirstField', field_id=1,
+ field_type=tracker_pb2.FieldTypes.STR_TYPE,
+ applicable_type='')
+ self.fd_2 = tracker_pb2.FieldDef(
+ field_name='SecField', field_id=2,
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ applicable_type='')
+ self.fd_3 = tracker_pb2.FieldDef(
+ field_name='LegalApproval', field_id=3,
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ applicable_type='')
+ self.fd_4 = tracker_pb2.FieldDef(
+ field_name='UserField', field_id=4,
+ field_type=tracker_pb2.FieldTypes.USER_TYPE,
+ applicable_type='')
+ self.fd_5 = tracker_pb2.FieldDef(
+ field_name='DogApproval', field_id=5,
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ applicable_type='')
+
+ def CallWrapped(self, wrapped_handler, *args, **kwargs):
+ return wrapped_handler.wrapped(self.issues_svcr, *args, **kwargs)
+
+ def testGetProjectIssueIDsAndConfig_OnlyOneProjectName(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ issue_refs = [
+ common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(local_id=2),
+ common_pb2.IssueRef(project_name='proj', local_id=3),
+ ]
+ project, issue_ids, config = self.issues_svcr._GetProjectIssueIDsAndConfig(
+ mc, issue_refs)
+ self.assertEqual(project, self.project)
+ self.assertEqual(issue_ids, [self.issue_1.issue_id, self.issue_2.issue_id])
+ self.assertEqual(
+ config,
+ self.services.config.GetProjectConfig(
+ self.cnxn, self.project.project_id))
+
+ def testGetProjectIssueIDsAndConfig_NoProjectName(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ issue_refs = [
+ common_pb2.IssueRef(local_id=2),
+ common_pb2.IssueRef(local_id=3),
+ ]
+ with self.assertRaises(exceptions.InputException):
+ self.issues_svcr._GetProjectIssueIDsAndConfig(mc, issue_refs)
+
+ def testGetProjectIssueIDsAndConfig_MultipleProjectNames(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ issue_refs = [
+ common_pb2.IssueRef(project_name='proj', local_id=2),
+ common_pb2.IssueRef(project_name='proj2', local_id=3),
+ ]
+ with self.assertRaises(exceptions.InputException):
+ self.issues_svcr._GetProjectIssueIDsAndConfig(mc, issue_refs)
+
+ def testGetProjectIssueIDsAndConfig_MissingLocalId(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ issue_refs = [
+ common_pb2.IssueRef(project_name='proj'),
+ common_pb2.IssueRef(project_name='proj', local_id=3),
+ ]
+ with self.assertRaises(exceptions.InputException):
+ self.issues_svcr._GetProjectIssueIDsAndConfig(mc, issue_refs)
+
+ def testCreateIssue_Normal(self):
+ """We can create an issue."""
+ request = issues_pb2.CreateIssueRequest(
+ project_name='proj',
+ issue=issue_objects_pb2.Issue(
+ project_name='proj', local_id=1, summary='sum'))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ response = self.CallWrapped(self.issues_svcr.CreateIssue, mc, request)
+
+ self.assertEqual('proj', response.project_name)
+
+ def testGetIssue_Normal(self):
+ """We can get an issue."""
+ request = issues_pb2.GetIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ response = self.CallWrapped(self.issues_svcr.GetIssue, mc, request)
+
+ actual = response.issue
+ self.assertEqual('proj', actual.project_name)
+ self.assertEqual(1, actual.local_id)
+ self.assertEqual(1, len(actual.blocked_on_issue_refs))
+ self.assertEqual('proj', actual.blocked_on_issue_refs[0].project_name)
+ self.assertEqual(2, actual.blocked_on_issue_refs[0].local_id)
+
+ def testGetIssue_Moved(self):
+ """We can get a moved issue."""
+ self.services.project.TestAddProject(
+ 'other', project_id=987, owner_ids=[111], contrib_ids=[111])
+ issue = fake.MakeTestIssue(987, 200, 'sum', 'New', 111, issue_id=1010)
+ self.services.issue.TestAddIssue(issue)
+ self.services.issue.TestAddMovedIssueRef(789, 404, 987, 200)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ request = issues_pb2.GetIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 404
+
+ response = self.CallWrapped(self.issues_svcr.GetIssue, mc, request)
+
+ ref = response.moved_to_ref
+ self.assertEqual(200, ref.local_id)
+ self.assertEqual('other', ref.project_name)
+
+ @patch('search.frontendsearchpipeline.FrontendSearchPipeline')
+ def testListIssues(self, mock_pipeline):
+ """We can get a list of issues from a search."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, [111])
+ config = self.services.config.GetProjectConfig(self.cnxn, 789)
+
+ instance = Mock(
+ spec=True, visible_results=[self.issue_1, self.issue_2],
+ users_by_id=users_by_id, harmonized_config=config,
+ pagination=Mock(total_count=2))
+ mock_pipeline.return_value = instance
+ instance.SearchForIIDs = Mock()
+ instance.MergeAndSortIssues = Mock()
+ instance.Paginate = Mock()
+
+ request = issues_pb2.ListIssuesRequest(query='',project_names=['proj'])
+ response = self.CallWrapped(self.issues_svcr.ListIssues, mc, request)
+
+ actual_issue_1 = response.issues[0]
+ self.assertEqual(actual_issue_1.owner_ref.user_id, 111)
+ self.assertEqual('owner@example.com', actual_issue_1.owner_ref.display_name)
+ self.assertEqual(actual_issue_1.local_id, 1)
+
+ actual_issue_2 = response.issues[1]
+ self.assertEqual(actual_issue_2.owner_ref.user_id, 111)
+ self.assertEqual('owner@example.com', actual_issue_2.owner_ref.display_name)
+ self.assertEqual(actual_issue_2.local_id, 2)
+ self.assertEqual(2, response.total_results)
+
+ # TODO(zhangtiff): Add tests for ListIssues + canned queries.
+
+ @patch('search.frontendsearchpipeline.FrontendSearchPipeline')
+ def testListIssues_IncludesAttachmentCount(self, mock_pipeline):
+ """Ensure ListIssues includes correct attachment counts."""
+
+ # Add an attachment to one of the issues so we can check attachment counts.
+ issue_3 = fake.MakeTestIssue(
+ 789, 3, 'sum', 'New', 111, project_name='proj', issue_id=2003,
+ attachment_count=1)
+ issue_4 = fake.MakeTestIssue(
+ 789, 4, 'sum', 'New', 111, project_name='proj', issue_id=2004,
+ attachment_count=-10)
+ self.services.issue.TestAddIssue(issue_3)
+ self.services.issue.TestAddIssue(issue_4)
+
+ # Request the list of issues.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+ mc.LookupLoggedInUserPerms(self.project)
+
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, [111])
+ config = self.services.config.GetProjectConfig(self.cnxn, 789)
+ instance = Mock(
+ spec=True, visible_results=[
+ self.issue_1, self.issue_2, issue_3, issue_4],
+ users_by_id=users_by_id, harmonized_config=config,
+ pagination=Mock(total_count=4))
+ mock_pipeline.return_value = instance
+ instance.SearchForIIDs = Mock()
+ instance.MergeAndSortIssues = Mock()
+ instance.Paginate = Mock()
+
+ request = issues_pb2.ListIssuesRequest(query='', project_names=['proj'])
+ response = self.CallWrapped(self.issues_svcr.ListIssues, mc, request)
+
+ # Ensure attachment counts match what we expect.
+ actual_issue_1 = response.issues[0]
+ self.assertEqual(actual_issue_1.attachment_count, 0)
+ self.assertEqual(actual_issue_1.local_id, 1)
+
+ actual_issue_2 = response.issues[1]
+ self.assertEqual(actual_issue_2.attachment_count, 0)
+ self.assertEqual(actual_issue_2.local_id, 2)
+
+ actual_issue_3 = response.issues[2]
+ self.assertEqual(actual_issue_3.attachment_count, 1)
+ self.assertEqual(actual_issue_3.local_id, 3)
+
+ actual_issue_4 = response.issues[3]
+ # NOTE(pawalls): It is not possible to test for presence in Proto3. Instead
+ # we test for default value here though it is semantically different
+ # and not quite the behavior we care about.
+ self.assertEqual(actual_issue_4.attachment_count, 0)
+ self.assertEqual(actual_issue_4.local_id, 4)
+
+ @patch('search.frontendsearchpipeline.FrontendSearchPipeline')
+ def testListIssues_No_visible_results(self, mock_pipeline):
+ """Ensure ListIssues handles the no visible results case."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=None, auth=None)
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, [111])
+ config = self.services.config.GetProjectConfig(self.cnxn, 789)
+
+ instance = Mock(
+ spec=True,
+ users_by_id=users_by_id,
+ harmonized_config=config,
+ # When there are no results, these default to None.
+ visible_results=None,
+ pagination=None)
+ mock_pipeline.return_value = instance
+ instance.SearchForIIDs = Mock()
+ instance.MergeAndSortIssues = Mock()
+ instance.Paginate = Mock()
+
+ request = issues_pb2.ListIssuesRequest(query='', project_names=['proj'])
+ response = self.CallWrapped(self.issues_svcr.ListIssues, mc, request)
+
+ self.assertEqual(len(response.issues), 0)
+
+ def testListReferencedIssues(self):
+ """We can get the referenced issues that exist."""
+ self.services.project.TestAddProject(
+ 'other-proj', project_id=788, owner_ids=[111])
+ other_issue = fake.MakeTestIssue(
+ 788, 1, 'sum', 'Fixed', 111, project_name='other-proj', issue_id=78801)
+ self.services.issue.TestAddIssue(other_issue)
+ # We ignore project_names or local_ids that don't exist in our DB.
+ request = issues_pb2.ListReferencedIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='other-proj', local_id=1),
+ common_pb2.IssueRef(project_name='other-proj', local_id=2),
+ common_pb2.IssueRef(project_name='ghost-proj', local_id=1)
+ ]
+ )
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ response = self.CallWrapped(
+ self.issues_svcr.ListReferencedIssues, mc, request)
+ self.assertEqual(len(response.closed_refs), 1)
+ self.assertEqual(len(response.open_refs), 1)
+ self.assertEqual(
+ issue_objects_pb2.Issue(
+ local_id=1,
+ project_name='other-proj',
+ summary='sum',
+ status_ref=common_pb2.StatusRef(
+ status='Fixed'),
+ owner_ref=common_pb2.UserRef(
+ user_id=111,
+ display_name='owner@example.com'),
+ reporter_ref=common_pb2.UserRef(
+ user_id=111,
+ display_name='owner@example.com')),
+ response.closed_refs[0])
+ self.assertEqual(
+ issue_objects_pb2.Issue(
+ local_id=1,
+ project_name='proj',
+ summary='sum',
+ status_ref=common_pb2.StatusRef(
+ status='New',
+ means_open=True),
+ owner_ref=common_pb2.UserRef(
+ user_id=111,
+ display_name='owner@example.com'),
+ blocked_on_issue_refs=[common_pb2.IssueRef(
+ project_name='proj',
+ local_id=2)],
+ reporter_ref=common_pb2.UserRef(
+ user_id=111,
+ display_name='owner@example.com'),
+ opened_timestamp=self.NOW,
+ component_modified_timestamp=self.NOW,
+ status_modified_timestamp=self.NOW,
+ owner_modified_timestamp=self.NOW),
+ response.open_refs[0])
+
+ def testListReferencedIssues_MissingInput(self):
+ request = issues_pb2.ListReferencedIssuesRequest(
+ issue_refs=[common_pb2.IssueRef(local_id=1)])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.ListReferencedIssues, mc, request)
+
+ def testListApplicableFieldDefs_EmptyIssueRefs(self):
+ request = issues_pb2.ListApplicableFieldDefsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.ListApplicableFieldDefs, mc, request)
+ self.assertEqual(response, issues_pb2.ListApplicableFieldDefsResponse())
+
+ def testListApplicableFieldDefs_CrossProjectRequest(self):
+ issue_refs = [common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj2', local_id=2)]
+ request = issues_pb2.ListApplicableFieldDefsRequest(issue_refs=issue_refs)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.ListApplicableFieldDefs, mc, request)
+
+ def testListApplicableFieldDefs_MissingProjectName(self):
+ issue_refs = [common_pb2.IssueRef(local_id=1),
+ common_pb2.IssueRef(local_id=2)]
+ request = issues_pb2.ListApplicableFieldDefsRequest(issue_refs=issue_refs)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.ListApplicableFieldDefs, mc, request)
+
+ def testListApplicableFieldDefs_Normal(self):
+ self.issue_1.labels = ['Type-Feedback']
+ self.issue_2.approval_values = [
+ tracker_pb2.ApprovalValue(approval_id=self.fd_3.field_id)]
+ self.fd_1.applicable_type = 'Defect' # not applicable
+ self.fd_2.applicable_type = 'feedback' # applicable
+ self.fd_3.applicable_type = 'ignored' # is APPROVAL_TYPE, applicable
+ self.fd_4.applicable_type = '' # applicable
+ self.fd_5.applicable_type = '' # is APPROVAl_TYPE, not applicable
+ config = tracker_pb2.ProjectIssueConfig(
+ project_id=789,
+ field_defs=[self.fd_1, self.fd_2, self.fd_3, self.fd_4, self.fd_5])
+ self.services.config.StoreConfig(self.cnxn, config)
+ issue_refs = [common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)]
+ request = issues_pb2.ListApplicableFieldDefsRequest(issue_refs=issue_refs)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.ListApplicableFieldDefs, mc, request)
+ converted_field_defs = [converters.ConvertFieldDef(fd, [], {}, config, True)
+ for fd in [self.fd_2, self.fd_3, self.fd_4]]
+ self.assertEqual(response, issues_pb2.ListApplicableFieldDefsResponse(
+ field_defs=converted_field_defs))
+
+ def testUpdateIssue_Denied_Edit(self):
+ """We reject requests to update an issue when the user lacks perms."""
+ request = issues_pb2.UpdateIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.delta.summary.value = 'new summary'
+
+ # Anon user can never update.
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # Signed in user cannot view this issue.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ self.issue_1.labels = ['Restrict-View-CoreTeam']
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # Signed in user cannot edit this issue.
+ self.issue_1.labels = ['Restrict-EditIssue-CoreTeam']
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ @patch('features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testUpdateIssue_JustAComment(self, _fake_pasicn):
+ """We check AddIssueComment when the user is only commenting."""
+ request = issues_pb2.UpdateIssueRequest()
+ request.comment_content = 'Foo'
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ # Note: no delta.
+
+ # Anon user can never update.
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # Signed in user cannot view this issue.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ self.issue_1.labels = ['Restrict-View-CoreTeam']
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # Signed in user cannot edit this issue, but they can still comment.
+ self.issue_1.labels = ['Restrict-EditIssue-CoreTeam']
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # Signed in user cannot post even a text comment.
+ self.issue_1.labels = ['Restrict-AddIssueComment-CoreTeam']
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ @patch('features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testUpdateIssue_Normal(self, fake_pasicn):
+ """We can update an issue."""
+ request = issues_pb2.UpdateIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.delta.summary.value = 'New summary'
+ request.delta.label_refs_add.extend([
+ common_pb2.LabelRef(label='Hot')])
+ request.comment_content = 'test comment'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ response = self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ actual = response.issue
+ # Intended stuff was changed.
+ self.assertEqual(1, len(actual.label_refs))
+ self.assertEqual('Hot', actual.label_refs[0].label)
+ self.assertEqual('New summary', actual.summary)
+
+ # Other stuff didn't change.
+ self.assertEqual('proj', actual.project_name)
+ self.assertEqual(1, actual.local_id)
+ self.assertEqual(1, len(actual.blocked_on_issue_refs))
+ self.assertEqual('proj', actual.blocked_on_issue_refs[0].project_name)
+ self.assertEqual(2, actual.blocked_on_issue_refs[0].local_id)
+
+ # A comment was added.
+ fake_pasicn.assert_called_once()
+ comments = self.services.issue.GetCommentsForIssue(
+ self.cnxn, self.issue_1.issue_id)
+ self.assertEqual(2, len(comments))
+ self.assertEqual('test comment', comments[1].content)
+
+ @patch('features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testUpdateIssue_CommentOnly(self, fake_pasicn):
+ """We can update an issue with a comment w/o making any other changes."""
+ request = issues_pb2.UpdateIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.comment_content = 'test comment'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # A comment was added.
+ fake_pasicn.assert_called_once()
+ comments = self.services.issue.GetCommentsForIssue(
+ self.cnxn, self.issue_1.issue_id)
+ self.assertEqual(2, len(comments))
+ self.assertEqual('test comment', comments[1].content)
+ self.assertFalse(comments[1].is_description)
+
+ @patch('features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testUpdateIssue_CommentWithAttachments(self, fake_pasicn):
+ """We can update an issue with a comment and attachments."""
+ request = issues_pb2.UpdateIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.comment_content = 'test comment'
+ request.uploads.extend([
+ issue_objects_pb2.AttachmentUpload(
+ filename='a.txt',
+ content='aaaaa')])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # A comment with an attachment was added.
+ fake_pasicn.assert_called_once()
+ comments = self.services.issue.GetCommentsForIssue(
+ self.cnxn, self.issue_1.issue_id)
+ self.assertEqual(2, len(comments))
+ self.assertEqual('test comment', comments[1].content)
+ self.assertFalse(comments[1].is_description)
+ self.assertEqual(1, len(comments[1].attachments))
+ self.assertEqual('a.txt', comments[1].attachments[0].filename)
+ self.assertEqual(5, self.project.attachment_bytes_used)
+
+ @patch('features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testUpdateIssue_Description(self, fake_pasicn):
+ """We can update an issue's description."""
+ request = issues_pb2.UpdateIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.comment_content = 'new description'
+ request.is_description = True
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # A comment was added.
+ fake_pasicn.assert_called_once()
+ comments = self.services.issue.GetCommentsForIssue(
+ self.cnxn, self.issue_1.issue_id)
+ self.assertEqual(2, len(comments))
+ self.assertEqual('new description', comments[1].content)
+ self.assertTrue(comments[1].is_description)
+
+ @patch('features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testUpdateIssue_NoOp(self, fake_pasicn):
+ """We gracefully ignore requests that have no delta or comment."""
+ request = issues_pb2.UpdateIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ response = self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ actual = response.issue
+ # Other stuff didn't change.
+ self.assertEqual('proj', actual.project_name)
+ self.assertEqual(1, actual.local_id)
+ self.assertEqual('sum', actual.summary)
+ self.assertEqual('New', actual.status_ref.status)
+
+ # No comment was added.
+ fake_pasicn.assert_not_called()
+ comments = self.services.issue.GetCommentsForIssue(
+ self.cnxn, self.issue_1.issue_id)
+ self.assertEqual(1, len(comments))
+
+ def testStarIssue_Denied(self):
+ """We reject requests to star an issue if the user lacks perms."""
+ request = issues_pb2.StarIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.starred = True
+
+ # Anon user cannot star an issue.
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.StarIssue, mc, request)
+
+ # User star an issue that they cannot view.
+ self.issue_1.labels = ['Restrict-View-CoreTeam']
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.StarIssue, mc, request)
+
+ # The issue was not actually starred.
+ self.assertEqual(0, self.issue_1.star_count)
+
+ def testStarIssue_Normal(self):
+ """Users can star and unstar issues."""
+ request = issues_pb2.StarIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.starred = True
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ # First, star it.
+ response = self.CallWrapped(self.issues_svcr.StarIssue, mc, request)
+ self.assertEqual(1, response.star_count)
+
+ # Then, unstar it.
+ request.starred = False
+ response = self.CallWrapped(self.issues_svcr.StarIssue, mc, request)
+ self.assertEqual(0, response.star_count)
+
+ def testIsIssueStared_Anon(self):
+ """Anon users can't star issues, so they always get back False."""
+ request = issues_pb2.IsIssueStarredRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ mc.LookupLoggedInUserPerms(self.project)
+
+ response = self.CallWrapped(self.issues_svcr.IsIssueStarred, mc, request)
+ self.assertFalse(response.is_starred)
+
+ def testIsIssueStared_Denied(self):
+ """Users can't ask about an issue that they cannot currently view."""
+ request = issues_pb2.IsIssueStarredRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ self.issue_1.labels = ['Restrict-View-CoreTeam']
+
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.IsIssueStarred, mc, request)
+
+ def testIsIssueStared_Normal(self):
+ """Users can star and unstar issues."""
+ request = issues_pb2.IsIssueStarredRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ # It is not initially starred by this user.
+ response = self.CallWrapped(self.issues_svcr.IsIssueStarred, mc, request)
+ self.assertFalse(response.is_starred)
+
+ # If we star it, we get response True.
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, 'fake config', self.issue_1.issue_id,
+ 333, True)
+ response = self.CallWrapped(self.issues_svcr.IsIssueStarred, mc, request)
+ self.assertTrue(response.is_starred)
+
+ def testListStarredIssues_Anon(self):
+ """Users can't see their starred issues until they sign in."""
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ mc.LookupLoggedInUserPerms(self.project)
+
+ response = self.CallWrapped(self.issues_svcr.ListStarredIssues, mc, {})
+ # Assert that response has an empty list
+ self.assertEqual(0, len(response.starred_issue_refs))
+
+ def testListStarredIssues_Normal(self):
+ """User can access which issues they've starred."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ # First, star some issues
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, 'fake config', self.issue_1.issue_id,
+ 333, True)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, 'fake config', self.issue_2.issue_id,
+ 333, True)
+
+ # Now test that user can retrieve their star in a list
+ response = self.CallWrapped(self.issues_svcr.ListStarredIssues, mc, {})
+ self.assertEqual(2, len(response.starred_issue_refs))
+
+ def testListComments_Normal(self):
+ """We can get comments on an issue."""
+ comment = tracker_pb2.IssueComment(
+ user_id=111, timestamp=self.NOW, content='second',
+ project_id=789, issue_id=self.issue_1.issue_id, sequence=1)
+ self.services.issue.TestAddComment(comment, self.issue_1.local_id)
+ request = issues_pb2.ListCommentsRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ response = self.CallWrapped(self.issues_svcr.ListComments, mc, request)
+
+ actual_0 = response.comments[0]
+ actual_1 = response.comments[1]
+ expected_0 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=0, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='owner@example.com'),
+ timestamp=self.NOW, content='sum', is_spam=False,
+ description_num=1, can_delete=True, can_flag=True)
+ expected_1 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=1, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='owner@example.com'),
+ timestamp=self.NOW, content='second', can_delete=True, can_flag=True)
+ self.assertEqual(expected_0, actual_0)
+ self.assertEqual(expected_1, actual_1)
+
+ def testListActivities_Normal(self):
+ """We can get issue activity."""
+ self.services.user.TestAddUser('user@example.com', 444)
+
+ config = tracker_pb2.ProjectIssueConfig(
+ project_id=789,
+ field_defs=[self.fd_1])
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ comment = tracker_pb2.IssueComment(
+ user_id=444, timestamp=self.NOW, content='c1',
+ project_id=789, issue_id=self.issue_1.issue_id, sequence=1)
+ self.services.issue.TestAddComment(comment, self.issue_1.local_id)
+
+ self.services.project.TestAddProject(
+ 'proj2', project_id=790, owner_ids=[111], contrib_ids=[222, 333])
+ issue_2 = fake.MakeTestIssue(
+ 790, 1, 'sum', 'New', 444, project_name='proj2',
+ opened_timestamp=self.NOW, issue_id=2001)
+ comment_2 = tracker_pb2.IssueComment(
+ user_id=444, timestamp=self.NOW, content='c2',
+ project_id=790, issue_id=issue_2.issue_id, sequence=1)
+ self.services.issue.TestAddComment(comment_2, issue_2.local_id)
+ self.services.issue.TestAddIssue(issue_2)
+
+ issue_3 = fake.MakeTestIssue(
+ 790, 2, 'sum', 'New', 111, project_name='proj2',
+ opened_timestamp=self.NOW, issue_id=2002, labels=['Restrict-View-Foo'])
+ comment_3 = tracker_pb2.IssueComment(
+ user_id=444, timestamp=self.NOW, content='c3',
+ project_id=790, issue_id=issue_3.issue_id, sequence=1)
+ self.services.issue.TestAddComment(comment_3, issue_3.local_id)
+ self.services.issue.TestAddIssue(issue_3)
+
+ request = issues_pb2.ListActivitiesRequest()
+ request.user_ref.user_id = 444
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.ListActivities, mc, request)
+
+ self.maxDiff = None
+ self.assertEqual([
+ issue_objects_pb2.Comment(
+ project_name='proj',
+ local_id=1,
+ commenter=common_pb2.UserRef(
+ user_id=444, display_name='user@example.com'),
+ timestamp=self.NOW,
+ content='c1',
+ sequence_num=1,
+ can_delete=True,
+ can_flag=True),
+ issue_objects_pb2.Comment(
+ project_name='proj2',
+ local_id=1,
+ commenter=common_pb2.UserRef(
+ user_id=444, display_name='user@example.com'),
+ timestamp=self.NOW,
+ content='sum',
+ description_num=1,
+ can_delete=True,
+ can_flag=True),
+ issue_objects_pb2.Comment(
+ project_name='proj2',
+ local_id=1,
+ commenter=common_pb2.UserRef(
+ user_id=444, display_name='user@example.com'),
+ timestamp=self.NOW,
+ content='c2',
+ sequence_num=1,
+ can_delete=True,
+ can_flag=True)],
+ sorted(
+ response.comments,
+ key=lambda c: (c.project_name, c.local_id, c.sequence_num)))
+ self.assertEqual([
+ issue_objects_pb2.IssueSummary(
+ project_name='proj',
+ local_id=1,
+ summary='sum'),
+ issue_objects_pb2.IssueSummary(
+ project_name='proj2',
+ local_id=1,
+ summary='sum')],
+ sorted(
+ response.issue_summaries,
+ key=lambda issue: (issue.project_name, issue.local_id)))
+
+ def testListActivities_Amendment(self):
+ self.services.user.TestAddUser('user@example.com', 444)
+
+ comment = tracker_pb2.IssueComment(
+ user_id=444,
+ timestamp=self.NOW,
+ amendments=[tracker_bizobj.MakeOwnerAmendment(111, 222)],
+ project_id=789,
+ issue_id=self.issue_1.issue_id,
+ content='',
+ sequence=1)
+ self.services.issue.TestAddComment(comment, self.issue_1.local_id)
+
+ request = issues_pb2.ListActivitiesRequest()
+ request.user_ref.user_id = 444
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.ListActivities, mc, request)
+
+ self.assertEqual([
+ issue_objects_pb2.Comment(
+ project_name='proj',
+ local_id=1,
+ commenter=common_pb2.UserRef(
+ user_id=444, display_name='user@example.com'),
+ timestamp=self.NOW,
+ content='',
+ sequence_num=1,
+ amendments=[issue_objects_pb2.Amendment(
+ field_name="Owner",
+ new_or_delta_value="ow...@example.com")],
+ can_delete=True,
+ can_flag=True)],
+ sorted(
+ response.comments,
+ key=lambda c: (c.project_name, c.local_id, c.sequence_num)))
+ self.assertEqual([
+ issue_objects_pb2.IssueSummary(
+ project_name='proj',
+ local_id=1,
+ summary='sum')],
+ sorted(
+ response.issue_summaries,
+ key=lambda issue: (issue.project_name, issue.local_id)))
+
+ @patch('testing.fake.IssueService.SoftDeleteComment')
+ def testDeleteComment_Invalid(self, fake_softdeletecomment):
+ """We reject requests to delete a non-existent comment."""
+ # Note: no comments added to self.issue_1 after the description.
+ request = issues_pb2.DeleteCommentRequest(
+ issue_ref=common_pb2.IssueRef(project_name='proj', local_id=1),
+ sequence_num=2, delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ with self.assertRaises(exceptions.NoSuchCommentException):
+ self.CallWrapped(self.issues_svcr.DeleteComment, mc, request)
+
+ fake_softdeletecomment.assert_not_called()
+
+ def testDeleteComment_Normal(self):
+ """An authorized user can delete and undelete a comment."""
+ comment_1 = tracker_pb2.IssueComment(
+ project_id=789, issue_id=self.issue_1.issue_id, content='one')
+ self.services.issue.TestAddComment(comment_1, 1)
+ comment_2 = tracker_pb2.IssueComment(
+ project_id=789, issue_id=self.issue_1.issue_id, content='two',
+ user_id=222)
+ self.services.issue.TestAddComment(comment_2, 1)
+
+ # Delete a comment.
+ request = issues_pb2.DeleteCommentRequest(
+ issue_ref=common_pb2.IssueRef(project_name='proj', local_id=1),
+ sequence_num=2, delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ response = self.CallWrapped(self.issues_svcr.DeleteComment, mc, request)
+
+ self.assertTrue(isinstance(response, empty_pb2.Empty))
+ self.assertEqual(111, comment_2.deleted_by)
+
+ # Undelete a comment.
+ request.delete=False
+
+ response = self.CallWrapped(self.issues_svcr.DeleteComment, mc, request)
+
+ self.assertTrue(isinstance(response, empty_pb2.Empty))
+ self.assertEqual(None, comment_2.deleted_by)
+
+ @patch('testing.fake.IssueService.SoftDeleteComment')
+ def testDeleteComment_Denied(self, fake_softdeletecomment):
+ """An unauthorized user cannot delete a comment."""
+ comment_1 = tracker_pb2.IssueComment(
+ project_id=789, issue_id=self.issue_1.issue_id, content='one',
+ user_id=222)
+ self.services.issue.TestAddComment(comment_1, 1)
+
+ request = issues_pb2.DeleteCommentRequest(
+ issue_ref=common_pb2.IssueRef(project_name='proj', local_id=1),
+ sequence_num=1, delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.DeleteComment, mc, request)
+
+ fake_softdeletecomment.assert_not_called()
+ self.assertIsNone(comment_1.deleted_by)
+
+ def testUpdateApproval_MissingFieldDef(self):
+ """Missing Approval Field Def throwns exception."""
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+ approval_delta = issue_objects_pb2.ApprovalDelta(
+ status=issue_objects_pb2.REVIEW_REQUESTED)
+ request = issues_pb2.UpdateApprovalRequest(
+ issue_ref=issue_ref, field_ref=field_ref, approval_delta=approval_delta)
+
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+
+ with self.assertRaises(exceptions.NoSuchFieldDefException):
+ self.CallWrapped(self.issues_svcr.UpdateApproval, mc, request)
+
+ def testBulkUpdateApprovals_EmptyIssueRefs(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ field_ref=common_pb2.FieldRef(field_name='LegalApproval'),
+ approval_delta=issue_objects_pb2.ApprovalDelta())
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.BulkUpdateApprovals, mc, request)
+
+ def testBulkUpdateApprovals_NoProjectName(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ issue_refs = [common_pb2.IssueRef(local_id=1),
+ common_pb2.IssueRef(local_id=2)]
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ issue_refs=issue_refs,
+ field_ref=common_pb2.FieldRef(field_name='LegalApproval'),
+ approval_delta=issue_objects_pb2.ApprovalDelta())
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.BulkUpdateApprovals, mc, request)
+
+ def testBulkUpdateApprovals_CrossProjectRequest(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ issue_refs = [common_pb2.IssueRef(project_name='p1', local_id=1),
+ common_pb2.IssueRef(project_name='p2', local_id=2)]
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ issue_refs=issue_refs,
+ field_ref=common_pb2.FieldRef(field_name='LegalApproval'),
+ approval_delta=issue_objects_pb2.ApprovalDelta())
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.BulkUpdateApprovals, mc, request)
+
+ def testBulkUpdateApprovals_NoSuchFieldDef(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ issue_refs = [common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)]
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ issue_refs=issue_refs,
+ field_ref=common_pb2.FieldRef(field_name='LegalApproval'),
+ approval_delta=issue_objects_pb2.ApprovalDelta())
+ with self.assertRaises(exceptions.NoSuchFieldDefException):
+ self.CallWrapped(self.issues_svcr.BulkUpdateApprovals, mc, request)
+
+ def testBulkUpdateApprovals_AnonDenied(self):
+ """Anon user cannot make any updates"""
+ config = tracker_pb2.ProjectIssueConfig(
+ project_id=789,
+ field_defs=[self.fd_3])
+ self.services.config.StoreConfig(self.cnxn, config)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+ approval_delta = issue_objects_pb2.ApprovalDelta()
+ issue_refs = [common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)]
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ issue_refs=issue_refs, field_ref=field_ref,
+ approval_delta=approval_delta)
+
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.BulkUpdateApprovals, mc, request)
+
+ def testBulkUpdateApprovals_UserLacksViewPerms(self):
+ """User who cannot view issue cannot update issue."""
+ config = tracker_pb2.ProjectIssueConfig(
+ project_id=789,
+ field_defs=[self.fd_3])
+ self.services.config.StoreConfig(self.cnxn, config)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+ approval_delta = issue_objects_pb2.ApprovalDelta()
+ issue_refs = [common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)]
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ issue_refs=issue_refs, field_ref=field_ref,
+ approval_delta=approval_delta)
+
+ self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.BulkUpdateApprovals, mc, request)
+
+ @patch('time.time')
+ @patch('businesslogic.work_env.WorkEnv.BulkUpdateIssueApprovals')
+ @patch('businesslogic.work_env.WorkEnv.GetIssueRefs')
+ def testBulkUpdateApprovals_Normal(
+ self, mockGetIssueRefs, mockBulkUpdateIssueApprovals, mockTime):
+ """Issue approvals that can be updated are updated and returned."""
+ mockTime.return_value = 12345
+ mockGetIssueRefs.return_value = {1001: ('proj', 1), 1002: ('proj', 2)}
+ config = tracker_pb2.ProjectIssueConfig(
+ project_id=789,
+ field_defs=[self.fd_3])
+ self.services.config.StoreConfig(self.cnxn, config)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+ issue_refs = [common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)]
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ issue_refs=issue_refs, field_ref=field_ref,
+ approval_delta=issue_objects_pb2.ApprovalDelta(
+ status=issue_objects_pb2.APPROVED),
+ comment_content='new bulk comment')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.BulkUpdateApprovals, mc, request)
+ self.assertEqual(
+ response,
+ issues_pb2.BulkUpdateApprovalsResponse(
+ issue_refs=[common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)]))
+
+ approval_delta = tracker_pb2.ApprovalDelta(
+ status=tracker_pb2.ApprovalStatus.APPROVED,
+ setter_id=444, set_on=12345)
+ mockBulkUpdateIssueApprovals.assert_called_once_with(
+ [1001, 1002], 3, self.project, approval_delta,
+ 'new bulk comment', send_email=False)
+
+ @patch('businesslogic.work_env.WorkEnv.BulkUpdateIssueApprovals')
+ @patch('businesslogic.work_env.WorkEnv.GetIssueRefs')
+ def testBulkUpdateApprovals_EmptyDelta(
+ self, mockGetIssueRefs, mockBulkUpdateIssueApprovals):
+ """Bulk update approval requests don't fail with an empty approval delta."""
+ mockGetIssueRefs.return_value = {1001: ('proj', 1)}
+ config = tracker_pb2.ProjectIssueConfig(
+ project_id=789,
+ field_defs=[self.fd_3])
+ self.services.config.StoreConfig(self.cnxn, config)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+ issue_refs = [common_pb2.IssueRef(project_name='proj', local_id=1)]
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ issue_refs=issue_refs, field_ref=field_ref,
+ comment_content='new bulk comment',
+ send_email=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+ self.CallWrapped(
+ self.issues_svcr.BulkUpdateApprovals, mc, request)
+
+ approval_delta = tracker_pb2.ApprovalDelta()
+ mockBulkUpdateIssueApprovals.assert_called_once_with(
+ [1001], 3, self.project, approval_delta,
+ 'new bulk comment', send_email=True)
+
+
+ @patch('businesslogic.work_env.WorkEnv.UpdateIssueApproval')
+ @patch('features.send_notifications.PrepareAndSendApprovalChangeNotification')
+ def testUpdateApproval(self, _mockPrepareAndSend, mockUpdateIssueApproval):
+ """We can update an approval."""
+
+ av_3 = tracker_pb2.ApprovalValue(
+ approval_id=3,
+ status=tracker_pb2.ApprovalStatus.NEEDS_REVIEW,
+ approver_ids=[333]
+ )
+ self.issue_1.approval_values = [av_3]
+
+ config = self.services.config.GetProjectConfig(
+ self.cnxn, 789)
+ config.field_defs = [self.fd_1, self.fd_3]
+
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+ approval_delta = issue_objects_pb2.ApprovalDelta(
+ status=issue_objects_pb2.REVIEW_REQUESTED,
+ approver_refs_add=[
+ common_pb2.UserRef(user_id=222, display_name='approver2@example.com')
+ ],
+ field_vals_add=[
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(field_name='FirstField'),
+ value='string')
+ ]
+ )
+
+ request = issues_pb2.UpdateApprovalRequest(
+ issue_ref=issue_ref, field_ref=field_ref, approval_delta=approval_delta,
+ comment_content='Well, actually'
+ )
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.uploads.extend([
+ issue_objects_pb2.AttachmentUpload(
+ filename='a.txt',
+ content='aaaaa')])
+ request.kept_attachments.extend([1, 2, 3])
+ request.send_email = True
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+
+ mockUpdateIssueApproval.return_value = [
+ tracker_pb2.ApprovalValue(
+ approval_id=3,
+ status=tracker_pb2.ApprovalStatus.REVIEW_REQUESTED,
+ setter_id=333,
+ approver_ids=[333, 222]),
+ 'comment_pb',
+ {}, # Fake issue.
+ ]
+
+ actual = self.CallWrapped(self.issues_svcr.UpdateApproval, mc, request)
+
+ expected = issues_pb2.UpdateApprovalResponse()
+ expected.approval.CopyFrom(
+ issue_objects_pb2.Approval(
+ field_ref=common_pb2.FieldRef(
+ field_id=3,
+ field_name='LegalApproval',
+ type=common_pb2.APPROVAL_TYPE),
+ approver_refs=[
+ common_pb2.UserRef(
+ user_id=333, display_name='approver3@example.com'),
+ common_pb2.UserRef(
+ user_id=222, display_name='approver2@example.com')
+ ],
+ status=issue_objects_pb2.REVIEW_REQUESTED,
+ setter_ref=common_pb2.UserRef(
+ user_id=333, display_name='approver3@example.com'),
+ phase_ref=issue_objects_pb2.PhaseRef()
+ )
+ )
+
+ work_env.WorkEnv(mc, self.services).UpdateIssueApproval.\
+ assert_called_once_with(
+ self.issue_1.issue_id, 3, ANY, u'Well, actually', False,
+ attachments=[(u'a.txt', 'aaaaa', 'text/plain')], send_email=True,
+ kept_attachments=[1, 2, 3])
+ self.assertEqual(expected, actual)
+
+ @patch('businesslogic.work_env.WorkEnv.UpdateIssueApproval')
+ @patch('features.send_notifications.PrepareAndSendApprovalChangeNotification')
+ def testUpdateApproval_IsDescription(
+ self, _mockPrepareAndSend, mockUpdateIssueApproval):
+ """We can update an approval survey."""
+
+ av_3 = tracker_pb2.ApprovalValue(approval_id=3)
+ self.issue_1.approval_values = [av_3]
+
+ config = self.services.config.GetProjectConfig(self.cnxn, 789)
+ config.field_defs = [self.fd_3]
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+ approval_delta = issue_objects_pb2.ApprovalDelta()
+
+ request = issues_pb2.UpdateApprovalRequest(
+ issue_ref=issue_ref, field_ref=field_ref, approval_delta=approval_delta,
+ comment_content='Better response.', is_description=True)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+
+ mockUpdateIssueApproval.return_value = [
+ tracker_pb2.ApprovalValue(approval_id=3),
+ 'comment_pb',
+ {}, # Fake issue.
+ ]
+
+ actual = self.CallWrapped(self.issues_svcr.UpdateApproval, mc, request)
+
+ expected = issues_pb2.UpdateApprovalResponse()
+ expected.approval.CopyFrom(
+ issue_objects_pb2.Approval(
+ field_ref=common_pb2.FieldRef(
+ field_id=3,
+ field_name='LegalApproval',
+ type=common_pb2.APPROVAL_TYPE),
+ phase_ref=issue_objects_pb2.PhaseRef()
+ )
+ )
+
+ work_env.WorkEnv(mc, self.services
+ ).UpdateIssueApproval.assert_called_once_with(
+ self.issue_1.issue_id, 3,
+ tracker_pb2.ApprovalDelta(),
+ u'Better response.', True, attachments=[], send_email=False,
+ kept_attachments=[])
+ self.assertEqual(expected, actual)
+
+ @patch('businesslogic.work_env.WorkEnv.UpdateIssueApproval')
+ @patch('features.send_notifications.PrepareAndSendApprovalChangeNotification')
+ def testUpdateApproval_EmptyDelta(
+ self, _mockPrepareAndSend, mockUpdateIssueApproval):
+ self.issue_1.approval_values = [tracker_pb2.ApprovalValue(approval_id=3)]
+
+ config = self.services.config.GetProjectConfig(self.cnxn, 789)
+ config.field_defs = [self.fd_3]
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+
+ request = issues_pb2.UpdateApprovalRequest(
+ issue_ref=issue_ref, field_ref=field_ref,
+ comment_content='Better response.', is_description=True)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+
+ mockUpdateIssueApproval.return_value = [
+ tracker_pb2.ApprovalValue(approval_id=3),
+ 'comment_pb',
+ {}, # Fake issue.
+ ]
+
+ actual = self.CallWrapped(self.issues_svcr.UpdateApproval, mc, request)
+
+ approval_value = issue_objects_pb2.Approval(
+ field_ref=common_pb2.FieldRef(
+ field_id=3,
+ field_name='LegalApproval',
+ type=common_pb2.APPROVAL_TYPE),
+ phase_ref=issue_objects_pb2.PhaseRef()
+ )
+ expected = issues_pb2.UpdateApprovalResponse(approval=approval_value)
+ self.assertEqual(expected, actual)
+
+ mockUpdateIssueApproval.assert_called_once_with(
+ self.issue_1.issue_id, 3,
+ tracker_pb2.ApprovalDelta(),
+ u'Better response.', True, attachments=[], send_email=False,
+ kept_attachments=[])
+
+ @patch('businesslogic.work_env.WorkEnv.ConvertIssueApprovalsTemplate')
+ def testConvertIssueApprovalsTemplate(self, mockWorkEnvConvertApprovals):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+ request = issues_pb2.ConvertIssueApprovalsTemplateRequest(
+ issue_ref=common_pb2.IssueRef(project_name='proj', local_id=1),
+ template_name='template_name', comment_content='CHICKEN',
+ send_email=True)
+ response = self.CallWrapped(
+ self.issues_svcr.ConvertIssueApprovalsTemplate, mc, request)
+ config = self.services.config.GetProjectConfig(self.cnxn, 789)
+ mockWorkEnvConvertApprovals.assert_called_once_with(
+ config, self.issue_1, 'template_name', request.comment_content,
+ send_email=request.send_email)
+ self.assertEqual(
+ response.issue,
+ issue_objects_pb2.Issue(
+ project_name='proj',
+ local_id=1,
+ summary='sum',
+ owner_ref=common_pb2.UserRef(
+ user_id=111, display_name='owner@example.com'),
+ status_ref=common_pb2.StatusRef(status='New', means_open=True),
+ blocked_on_issue_refs=[
+ common_pb2.IssueRef(project_name='proj', local_id=2)],
+ reporter_ref=common_pb2.UserRef(
+ user_id=111, display_name='owner@example.com'),
+ opened_timestamp=self.NOW,
+ component_modified_timestamp=self.NOW,
+ status_modified_timestamp=self.NOW,
+ owner_modified_timestamp=self.NOW,
+ ))
+
+ def testConvertIssueApprovalsTemplate_MissingRequiredFields(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+ request = issues_pb2.ConvertIssueApprovalsTemplateRequest(
+ issue_ref=common_pb2.IssueRef(project_name='proj', local_id=1))
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(
+ self.issues_svcr.ConvertIssueApprovalsTemplate, mc, request)
+
+ request = issues_pb2.ConvertIssueApprovalsTemplateRequest(
+ template_name='name')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(
+ self.issues_svcr.ConvertIssueApprovalsTemplate, mc, request)
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_RequiredFields(self, mockSnapshotCountsQuery):
+ """Test that timestamp is required at all times.
+ And that label_prefix is required when group_by is 'label'.
+ """
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ # Test timestamp is required.
+ request = issues_pb2.IssueSnapshotRequest(project_name='proj')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ # Test project_name or hotlist_id is required.
+ request = issues_pb2.IssueSnapshotRequest(timestamp=1531334109)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ # Test label_prefix is required when group_by is 'label'.
+ request = issues_pb2.IssueSnapshotRequest(timestamp=1531334109,
+ project_name='proj', group_by='label')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ mockSnapshotCountsQuery.assert_not_called()
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_Basic(self, mockSnapshotCountsQuery):
+ """Tests the happy path case."""
+ request = issues_pb2.IssueSnapshotRequest(
+ timestamp=1531334109, project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSnapshotCountsQuery.return_value = ({'total': 123}, [], True)
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(123, response.snapshot_count[0].count)
+ self.assertEqual(0, len(response.unsupported_field))
+ self.assertTrue(response.search_limit_reached)
+ mockSnapshotCountsQuery.assert_called_once_with(self.project, 1531334109,
+ '', query=None, canned_query=None, label_prefix='', hotlist=None)
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ @patch('search.searchpipeline.ReplaceKeywordsWithUserIDs')
+ @patch('features.savedqueries_helpers.SavedQueryIDToCond')
+ def testSnapshotCounts_ReplacesKeywords(self, mockSavedQueryIDToCond,
+ mockReplaceKeywordsWithUserIDs,
+ mockSnapshotCountsQuery):
+ """Tests that canned query is unpacked and keywords in query and canned
+ query are replaced with user IDs."""
+ request = issues_pb2.IssueSnapshotRequest(timestamp=1531334109,
+ project_name='proj', query='owner:me', canned_query=3)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSavedQueryIDToCond.return_value = 'cc:me'
+ mockReplaceKeywordsWithUserIDs.side_effect = [
+ ('cc:2345', []), ('owner:1234', [])]
+ mockSnapshotCountsQuery.return_value = ({'total': 789}, [], False)
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(789, response.snapshot_count[0].count)
+ self.assertEqual(0, len(response.unsupported_field))
+ self.assertFalse(response.search_limit_reached)
+ mockSnapshotCountsQuery.assert_called_once_with(self.project, 1531334109,
+ '', query='owner:1234', canned_query='cc:2345', label_prefix='',
+ hotlist=None)
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_GroupByLabel(self, mockSnapshotCountsQuery):
+ """Tests grouping by label with label_prefix and a query.
+ But no canned_query.
+ """
+ request = issues_pb2.IssueSnapshotRequest(timestamp=1531334109,
+ project_name='proj', group_by='label', label_prefix='Type',
+ query='rutabaga:rutabaga')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSnapshotCountsQuery.return_value = (
+ {'label1': 123, 'label2': 987},
+ ['rutabaga'],
+ True)
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(2, len(response.snapshot_count))
+ self.assertEqual('label1', response.snapshot_count[0].dimension)
+ self.assertEqual(123, response.snapshot_count[0].count)
+ self.assertEqual('label2', response.snapshot_count[1].dimension)
+ self.assertEqual(987, response.snapshot_count[1].count)
+ self.assertEqual(1, len(response.unsupported_field))
+ self.assertEqual('rutabaga', response.unsupported_field[0])
+ self.assertTrue(response.search_limit_reached)
+ mockSnapshotCountsQuery.assert_called_once_with(self.project, 1531334109,
+ 'label', label_prefix='Type', query='rutabaga:rutabaga',
+ canned_query=None, hotlist=None)
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_GroupByComponent(self, mockSnapshotCountsQuery):
+ """Tests grouping by component with a query and a canned_query."""
+ request = issues_pb2.IssueSnapshotRequest(timestamp=1531334109,
+ project_name='proj', group_by='component',
+ query='rutabaga:rutabaga', canned_query=2)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSnapshotCountsQuery.return_value = (
+ {'component1': 123, 'component2': 987},
+ ['rutabaga'],
+ True)
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(2, len(response.snapshot_count))
+ self.assertEqual('component1', response.snapshot_count[0].dimension)
+ self.assertEqual(123, response.snapshot_count[0].count)
+ self.assertEqual('component2', response.snapshot_count[1].dimension)
+ self.assertEqual(987, response.snapshot_count[1].count)
+ self.assertEqual(1, len(response.unsupported_field))
+ self.assertEqual('rutabaga', response.unsupported_field[0])
+ self.assertTrue(response.search_limit_reached)
+ mockSnapshotCountsQuery.assert_called_once_with(self.project, 1531334109,
+ 'component', label_prefix='', query='rutabaga:rutabaga',
+ canned_query='is:open', hotlist=None)
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_GroupByOpen(self, mockSnapshotCountsQuery):
+ """Tests grouping by open with a query."""
+ request = issues_pb2.IssueSnapshotRequest(
+ timestamp=1531334109, project_name='proj', group_by='open')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSnapshotCountsQuery.return_value = (
+ {'Opened': 100, 'Closed': 23}, [], True)
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(2, len(response.snapshot_count))
+ self.assertEqual('Opened', response.snapshot_count[0].dimension)
+ self.assertEqual(100, response.snapshot_count[0].count)
+ self.assertEqual('Closed', response.snapshot_count[1].dimension)
+ self.assertEqual(23, response.snapshot_count[1].count)
+ mockSnapshotCountsQuery.assert_called_once_with(self.project, 1531334109,
+ 'open', label_prefix='', query=None, canned_query=None, hotlist=None)
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_GroupByStatus(self, mockSnapshotCountsQuery):
+ """Tests grouping by status with a query."""
+ request = issues_pb2.IssueSnapshotRequest(
+ timestamp=1531334109, project_name='proj', group_by='status')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSnapshotCountsQuery.return_value = (
+ {'Accepted': 100, 'Fixed': 23}, [], True)
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(2, len(response.snapshot_count))
+ self.assertEqual('Fixed', response.snapshot_count[0].dimension)
+ self.assertEqual(23, response.snapshot_count[0].count)
+ self.assertEqual('Accepted', response.snapshot_count[1].dimension)
+ self.assertEqual(100, response.snapshot_count[1].count)
+ mockSnapshotCountsQuery.assert_called_once_with(self.project, 1531334109,
+ 'status', label_prefix='', query=None, canned_query=None, hotlist=None)
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_GroupByOwner(self, mockSnapshotCountsQuery):
+ """Tests grouping by status with a query."""
+ request = issues_pb2.IssueSnapshotRequest(
+ timestamp=1531334109, project_name='proj', group_by='owner')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSnapshotCountsQuery.return_value = ({111: 100}, [], True)
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(1, len(response.snapshot_count))
+ self.assertEqual('owner@example.com', response.snapshot_count[0].dimension)
+ self.assertEqual(100, response.snapshot_count[0].count)
+ mockSnapshotCountsQuery.assert_called_once_with(self.project, 1531334109,
+ 'owner', label_prefix='', query=None, canned_query=None, hotlist=None)
+
+ @patch('businesslogic.work_env.WorkEnv.GetHotlist')
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_WithHotlist(self, mockSnapshotCountsQuery,
+ mockGetHotlist):
+ """Tests grouping by status with a hotlist."""
+ request = issues_pb2.IssueSnapshotRequest(
+ timestamp=1531334109, hotlist_id=19191)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSnapshotCountsQuery.return_value = ({'total': 123}, [], True)
+ fake_hotlist = fake.Hotlist('hotlist_rutabaga', 19191)
+ mockGetHotlist.return_value = fake_hotlist
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(1, len(response.snapshot_count))
+ self.assertEqual('total', response.snapshot_count[0].dimension)
+ self.assertEqual(123, response.snapshot_count[0].count)
+ mockSnapshotCountsQuery.assert_called_once_with(None, 1531334109,
+ '', label_prefix='', query=None, canned_query=None,
+ hotlist=fake_hotlist)
+
+ def AddField(self, name, field_type_str):
+ kwargs = {
+ 'cnxn': self.cnxn,
+ 'project_id': self.project.project_id,
+ 'field_name': name,
+ 'field_type_str': field_type_str}
+ kwargs.update(
+ {
+ arg: None for arg in (
+ 'applic_type', 'applic_pred', 'is_required', 'is_niche',
+ 'is_multivalued', 'min_value', 'max_value', 'regex',
+ 'needs_member', 'needs_perm', 'grants_perm', 'notify_on',
+ 'date_action_str', 'docstring')
+ })
+ kwargs.update({arg: [] for arg in ('admin_ids', 'editor_ids')})
+
+ return self.services.config.CreateFieldDef(**kwargs)
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_NoDerivedFields(self, mockGetFilterRules):
+ """When no rules match, we respond with just owner availability."""
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111),
+ label_refs_add=[common_pb2.LabelRef(label='foo')])
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule('label:bar', add_labels=['baz'])]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ issues_pb2.PresubmitIssueResponse(
+ owner_availability="User never visited",
+ owner_availability_state="never"),
+ response)
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_IncompleteOwnerEmail(self, mockGetFilterRules):
+ """User is in the process of typing in the proposed owner."""
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(display_name='owner@examp'))
+
+ mockGetFilterRules.return_value = []
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ actual = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ issues_pb2.PresubmitIssueResponse(),
+ actual)
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_NewIssue(self, mockGetFilterRules):
+ """Proposed owner has a vacation message set."""
+ self.user_1.vacation_message = 'In Galapagos Islands'
+ issue_ref = common_pb2.IssueRef(project_name='proj')
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111),
+ label_refs_add=[common_pb2.LabelRef(label='foo')])
+
+ mockGetFilterRules.return_value = []
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ issues_pb2.PresubmitIssueResponse(
+ owner_availability='In Galapagos Islands',
+ owner_availability_state='none'),
+ response)
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_OwnerVacation(self, mockGetFilterRules):
+ """Proposed owner has a vacation message set."""
+ self.user_1.vacation_message = 'In Galapagos Islands'
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111),
+ label_refs_add=[common_pb2.LabelRef(label='foo')])
+
+ mockGetFilterRules.return_value = []
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ issues_pb2.PresubmitIssueResponse(
+ owner_availability='In Galapagos Islands',
+ owner_availability_state='none'),
+ response)
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_OwnerIsAvailable(self, mockGetFilterRules):
+ """Proposed owner not on vacation and has visited recently."""
+ self.user_1.last_visit_timestamp = int(time.time())
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111),
+ label_refs_add=[common_pb2.LabelRef(label='foo')])
+
+ mockGetFilterRules.return_value = []
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ issues_pb2.PresubmitIssueResponse(
+ owner_availability='',
+ owner_availability_state=''),
+ response)
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_DerivedLabels(self, mockGetFilterRules):
+ """Test that we can match label rules and return derived labels."""
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111),
+ label_refs_add=[common_pb2.LabelRef(label='foo')])
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule('label:foo', add_labels=['bar', 'baz'])]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ [common_pb2.ValueAndWhy(
+ value='bar',
+ why='Added by rule: IF label:foo THEN ADD LABEL'),
+ common_pb2.ValueAndWhy(
+ value='baz',
+ why='Added by rule: IF label:foo THEN ADD LABEL')],
+ [vnw for vnw in response.derived_labels])
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_DerivedOwner(self, mockGetFilterRules):
+ """Test that we can match component rules and return derived owners."""
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Foo', 'Foo Docstring', False,
+ [], [], 0, 111, [])
+ self.issue_1.owner_id = 0
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ comp_refs_add=[common_pb2.ComponentRef(path='Foo')])
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule('component:Foo', default_owner_id=222)]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ [common_pb2.ValueAndWhy(
+ value='approver2@example.com',
+ why='Added by rule: IF component:Foo THEN SET DEFAULT OWNER')],
+ [vnw for vnw in response.derived_owners])
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_DerivedCCs(self, mockGetFilterRules):
+ """Test that we can match field rules and return derived cc emails."""
+ field_id = self.AddField('Foo', 'ENUM_TYPE')
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111),
+ field_vals_add=[issue_objects_pb2.FieldValue(
+ value='Bar', field_ref=common_pb2.FieldRef(field_id=field_id))])
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule('Foo=Bar', add_cc_ids=[222, 333])]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ [common_pb2.ValueAndWhy(
+ value='approver2@example.com',
+ why='Added by rule: IF Foo=Bar THEN ADD CC'),
+ common_pb2.ValueAndWhy(
+ value='approver3@example.com',
+ why='Added by rule: IF Foo=Bar THEN ADD CC')],
+ [vnw for vnw in response.derived_ccs])
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_DerivedCCsNonMember(self, mockGetFilterRules):
+ """Test that we can return obscured cc emails to non-members."""
+ field_id = self.AddField('Foo', 'ENUM_TYPE')
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111),
+ field_vals_add=[issue_objects_pb2.FieldValue(
+ value='Bar', field_ref=common_pb2.FieldRef(field_id=field_id))])
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule('Foo=Bar', add_cc_ids=[222, 333])]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ [
+ common_pb2.ValueAndWhy(
+ value='appro...@example.com',
+ why='Added by rule: IF Foo=Bar THEN ADD CC'),
+ common_pb2.ValueAndWhy(
+ value='appro...@example.com',
+ why='Added by rule: IF Foo=Bar THEN ADD CC')
+ ], [vnw for vnw in response.derived_ccs])
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_Warnings(self, mockGetFilterRules):
+ """Test that we can match owner rules and return warnings."""
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111))
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule(
+ 'owner:owner@example.com', warning='Owner is too busy')]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ [common_pb2.ValueAndWhy(
+ value='Owner is too busy',
+ why='Added by rule: IF owner:owner@example.com THEN ADD WARNING')],
+ [vnw for vnw in response.warnings])
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_Errors(self, mockGetFilterRules):
+ """Test that we can match owner rules and return errors."""
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=222),
+ cc_refs_add=[
+ common_pb2.UserRef(user_id=111),
+ common_pb2.UserRef(user_id=333)])
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule(
+ 'cc:owner@example.com', error='Owner is not to be disturbed')]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ [common_pb2.ValueAndWhy(
+ value='Owner is not to be disturbed',
+ why='Added by rule: IF cc:owner@example.com THEN ADD ERROR')],
+ [vnw for vnw in response.errors])
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_Errors_ExistingOwner(self, mockGetFilterRules):
+ """Test that we apply the rules to the issue + delta, not only delta."""
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta()
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule(
+ 'owner:owner@example.com', error='Owner is not to be disturbed')]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ [common_pb2.ValueAndWhy(
+ value='Owner is not to be disturbed',
+ why='Added by rule: IF owner:owner@example.com THEN ADD ERROR')],
+ [vnw for vnw in response.errors])
+
+ def testRerankBlockedOnIssues_SplitBelow(self):
+ issues = []
+ for idx in range(3, 6):
+ issues.append(fake.MakeTestIssue(
+ 789, idx, 'sum', 'New', 111, project_name='proj', issue_id=1000+idx))
+ self.services.issue.TestAddIssue(issues[-1])
+ self.issue_1.blocked_on_iids.append(issues[-1].issue_id)
+ self.issue_1.blocked_on_ranks.append(self.issue_1.blocked_on_ranks[-1]-1)
+
+ request = issues_pb2.RerankBlockedOnIssuesRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ moved_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=2),
+ target_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=4),
+ split_above=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.RerankBlockedOnIssues, mc, request)
+
+ self.assertEqual(
+ [3, 4, 2, 5],
+ [blocked_on_ref.local_id
+ for blocked_on_ref in response.blocked_on_issue_refs])
+
+ def testRerankBlockedOnIssues_SplitAbove(self):
+ self.project.committer_ids.append(222)
+ issues = []
+ for idx in range(3, 6):
+ issues.append(fake.MakeTestIssue(
+ 789, idx, 'sum', 'New', 111, project_name='proj', issue_id=1000+idx))
+ self.services.issue.TestAddIssue(issues[-1])
+ self.issue_1.blocked_on_iids.append(issues[-1].issue_id)
+ self.issue_1.blocked_on_ranks.append(self.issue_1.blocked_on_ranks[-1]-1)
+
+ request = issues_pb2.RerankBlockedOnIssuesRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ moved_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=2),
+ target_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=4),
+ split_above=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver2@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.RerankBlockedOnIssues, mc, request)
+
+ self.assertEqual(
+ [3, 2, 4, 5],
+ [blocked_on_ref.local_id
+ for blocked_on_ref in response.blocked_on_issue_refs])
+
+ def testRerankBlockedOnIssues_CantEditIssue(self):
+ self.project.committer_ids.append(222)
+ issues = []
+ for idx in range(3, 6):
+ issues.append(fake.MakeTestIssue(
+ 789, idx, 'sum', 'New', 111, project_name='proj', issue_id=1000+idx))
+ self.services.issue.TestAddIssue(issues[-1])
+ self.issue_1.blocked_on_iids.append(issues[-1].issue_id)
+ self.issue_1.blocked_on_ranks.append(self.issue_1.blocked_on_ranks[-1]-1)
+
+ self.issue_1.labels = ['Restrict-EditIssue-Foo']
+
+ request = issues_pb2.RerankBlockedOnIssuesRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ moved_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=2),
+ target_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=4),
+ split_above=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver2@example.com')
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.RerankBlockedOnIssues, mc, request)
+
+ def testRerankBlockedOnIssues_ComplexPermissions(self):
+ """We can rerank blocked on issues, regardless of perms on other issues.
+
+ If Issue 1 is blocked on Issue 3 and Issue 4, we should be able to reorder
+ them as long as we have permission to edit Issue 1, even if we don't have
+ permission to view or edit Issues 3 or 4.
+ """
+ # Issue 3 is in proj2, which we don't have access to.
+ project_2 = self.services.project.TestAddProject(
+ 'proj2', project_id=790, owner_ids=[222], contrib_ids=[333])
+ project_2.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ issue_3 = fake.MakeTestIssue(
+ 790, 3, 'sum', 'New', 111, project_name='proj2', issue_id=1003)
+
+ # Issue 4 requires a permission we don't have in order to edit it.
+ issue_4 = fake.MakeTestIssue(
+ 789, 4, 'sum', 'New', 111, project_name='proj', issue_id=1004)
+ issue_4.labels = ['Restrict-EditIssue-Foo']
+
+ self.services.issue.TestAddIssue(issue_3)
+ self.services.issue.TestAddIssue(issue_4)
+
+ self.issue_1.blocked_on_iids = [1003, 1004]
+ self.issue_1.blocked_on_ranks = [2, 1]
+
+ request = issues_pb2.RerankBlockedOnIssuesRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ moved_ref=common_pb2.IssueRef(
+ project_name='proj2',
+ local_id=3),
+ target_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=4),
+ split_above=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.RerankBlockedOnIssues, mc, request)
+
+ self.assertEqual(
+ [4, 3],
+ [blocked_on_ref.local_id
+ for blocked_on_ref in response.blocked_on_issue_refs])
+
+ def testDeleteIssue_Delete(self):
+ """We can delete an issue."""
+ issue = self.services.issue.GetIssue(self.cnxn, self.issue_1.issue_id)
+ self.assertFalse(issue.deleted)
+
+ request = issues_pb2.DeleteIssueRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.DeleteIssue, mc, request)
+
+ issue = self.services.issue.GetIssue(self.cnxn, self.issue_1.issue_id)
+ self.assertTrue(issue.deleted)
+
+ def testDeleteIssue_Undelete(self):
+ """We can undelete an issue."""
+ self.services.issue.SoftDeleteIssue(
+ self.cnxn, self.project.project_id, 1, True, self.services.user)
+ issue = self.services.issue.GetIssue(self.cnxn, self.issue_1.issue_id)
+ self.assertTrue(issue.deleted)
+
+ request = issues_pb2.DeleteIssueRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ delete=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.DeleteIssue, mc, request)
+
+ issue = self.services.issue.GetIssue(self.cnxn, self.issue_1.issue_id)
+ self.assertFalse(issue.deleted)
+
+ def testDeleteIssueComment_Delete(self):
+ """We can delete an issue comment."""
+ comment = tracker_pb2.IssueComment(
+ project_id=self.project.project_id,
+ issue_id=self.issue_1.issue_id,
+ user_id=111,
+ content='Foo',
+ timestamp=12345)
+ self.services.issue.TestAddComment(comment, self.issue_1.local_id)
+
+ request = issues_pb2.DeleteIssueCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.DeleteIssueComment, mc, request)
+
+ comment = self.services.issue.GetComment(self.cnxn, comment.id)
+ self.assertEqual(111, comment.deleted_by)
+
+ def testDeleteIssueComment_Undelete(self):
+ """We can undelete an issue comment."""
+ comment = tracker_pb2.IssueComment(
+ project_id=self.project.project_id,
+ issue_id=self.issue_1.issue_id,
+ user_id=111,
+ content='Foo',
+ timestamp=12345,
+ deleted_by=111)
+ self.services.issue.TestAddComment(comment, self.issue_1.local_id)
+
+ request = issues_pb2.DeleteIssueCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ delete=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.DeleteIssueComment, mc, request)
+
+ comment = self.services.issue.GetComment(self.cnxn, comment.id)
+ self.assertIsNone(comment.deleted_by)
+
+ def testDeleteIssueComment_InvalidSequenceNum(self):
+ """We can handle invalid sequence numbers."""
+ request = issues_pb2.DeleteIssueCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.DeleteIssueComment, mc, request)
+
+ def testDeleteAttachment_Delete(self):
+ """We can delete an issue comment attachment."""
+ comment = tracker_pb2.IssueComment(
+ project_id=self.project.project_id,
+ issue_id=self.issue_1.issue_id,
+ user_id=111,
+ content='Foo',
+ timestamp=12345)
+ self.services.issue.TestAddComment(comment, self.issue_1.local_id)
+ attachment = tracker_pb2.Attachment()
+ self.services.issue.TestAddAttachment(attachment, comment.id, 1)
+
+ request = issues_pb2.DeleteAttachmentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ attachment_id=attachment.attachment_id,
+ delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(
+ self.issues_svcr.DeleteAttachment, mc, request)
+
+ self.assertTrue(attachment.deleted)
+
+ def testDeleteAttachment_Undelete(self):
+ """We can undelete an issue comment attachment."""
+ comment = tracker_pb2.IssueComment(
+ project_id=self.project.project_id,
+ issue_id=self.issue_1.issue_id,
+ user_id=111,
+ content='Foo',
+ timestamp=12345,
+ deleted_by=111)
+ self.services.issue.TestAddComment(comment, self.issue_1.local_id)
+ attachment = tracker_pb2.Attachment(deleted=True)
+ self.services.issue.TestAddAttachment(attachment, comment.id, 1)
+
+ request = issues_pb2.DeleteAttachmentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ attachment_id=attachment.attachment_id,
+ delete=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(
+ self.issues_svcr.DeleteAttachment, mc, request)
+
+ self.assertFalse(attachment.deleted)
+
+ def testDeleteAttachment_InvalidSequenceNum(self):
+ """We can handle invalid sequence numbers."""
+ request = issues_pb2.DeleteAttachmentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ attachment_id=1234,
+ delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(
+ self.issues_svcr.DeleteAttachment, mc, request)
+
+ def testFlagIssues_Normal(self):
+ """Test that an user can flag an issue as spam."""
+ self.services.user.TestAddUser('user@example.com', 999)
+
+ request = issues_pb2.FlagIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=2)],
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ issue_id = self.issue_1.issue_id
+ self.assertEqual(
+ [999], self.services.spam.reports_by_issue_id[issue_id])
+ self.assertNotIn(
+ 999, self.services.spam.manual_verdicts_by_issue_id[issue_id])
+
+ issue_id2 = self.issue_2.issue_id
+ self.assertEqual(
+ [999], self.services.spam.reports_by_issue_id[issue_id2])
+ self.assertNotIn(
+ 999, self.services.spam.manual_verdicts_by_issue_id[issue_id2])
+
+ def testFlagIssues_Unflag(self):
+ """Test that we can un-flag an issue as spam."""
+ self.services.spam.FlagIssues(
+ self.cnxn, self.services.issue, [self.issue_1], 111, True)
+ self.services.spam.RecordManualIssueVerdicts(
+ self.cnxn, self.services.issue, [self.issue_1], 111, True)
+
+ request = issues_pb2.FlagIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1)],
+ flag=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ issue_id = self.issue_1.issue_id
+ self.assertEqual([], self.services.spam.reports_by_issue_id[issue_id])
+ self.assertFalse(
+ self.services.spam.manual_verdicts_by_issue_id[issue_id][111])
+
+ def testFlagIssues_OwnerAutoVerdict(self):
+ """Test that an owner can flag an issue as spam and it is a verdict."""
+ request = issues_pb2.FlagIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1)],
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ issue_id = self.issue_1.issue_id
+ self.assertEqual(
+ [111], self.services.spam.reports_by_issue_id[issue_id])
+ self.assertTrue(
+ self.services.spam.manual_verdicts_by_issue_id[issue_id][111])
+
+ def testFlagIssues_CommitterAutoVerdict(self):
+ """Test that an owner can flag an issue as spam and it is a verdict."""
+ self.services.user.TestAddUser('committer@example.com', 999)
+ self.services.project.TestAddProjectMembers(
+ [999], self.project, fake.COMMITTER_ROLE)
+
+ request = issues_pb2.FlagIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1)],
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='committer@example.com')
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ issue_id = self.issue_1.issue_id
+ self.assertEqual(
+ [999], self.services.spam.reports_by_issue_id[issue_id])
+ self.assertTrue(
+ self.services.spam.manual_verdicts_by_issue_id[issue_id][999])
+
+ def testFlagIssues_ContributorAutoVerdict(self):
+ """Test that an owner can flag an issue as spam and it is a verdict."""
+ request = issues_pb2.FlagIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1)],
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver2@example.com')
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ issue_id = self.issue_1.issue_id
+ self.assertEqual(
+ [222], self.services.spam.reports_by_issue_id[issue_id])
+ self.assertTrue(
+ self.services.spam.manual_verdicts_by_issue_id[issue_id][222])
+
+ def testFlagIssues_NotAllowed(self):
+ """Test that anon users cannot flag issues as spam."""
+ request = issues_pb2.FlagIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1)],
+ flag=True)
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ self.assertEqual(
+ [], self.services.spam.reports_by_issue_id[self.issue_1.issue_id])
+ self.assertEqual({}, self.services.spam.manual_verdicts_by_issue_id)
+
+ def testFlagIssues_CrossProjectNotAllowed(self):
+ """Test that cross-project requests are rejected."""
+ request = issues_pb2.FlagIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ common_pb2.IssueRef(
+ project_name='proj2',
+ local_id=2)],
+ flag=True)
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ self.assertEqual(
+ [], self.services.spam.reports_by_issue_id[self.issue_1.issue_id])
+ self.assertEqual({}, self.services.spam.manual_verdicts_by_issue_id)
+
+ def testFlagIssues_MissingIssueRefs(self):
+ request = issues_pb2.FlagIssuesRequest(flag=True)
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ def testFlagComment_InvalidSequenceNumber(self):
+ """Test that we reject requests with invalid sequence numbers."""
+ request = issues_pb2.FlagCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.FlagComment, mc, request)
+
+ def testFlagComment_Normal(self):
+ """Test that an user can flag a comment as spam."""
+ self.services.user.TestAddUser('user@example.com', 999)
+ comment = tracker_pb2.IssueComment(
+ project_id=789, content='soon to be deleted', user_id=111,
+ issue_id=self.issue_1.issue_id)
+ self.services.issue.TestAddComment(comment, 1)
+
+ request = issues_pb2.FlagCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ self.CallWrapped(self.issues_svcr.FlagComment, mc, request)
+
+ comment_reports = self.services.spam.comment_reports_by_issue_id
+ manual_verdicts = self.services.spam.manual_verdicts_by_comment_id
+ self.assertEqual([999], comment_reports[self.issue_1.issue_id][comment.id])
+ self.assertNotIn(999, manual_verdicts[comment.id])
+
+ def testFlagComment_Unflag(self):
+ """Test that we can un-flag a comment as spam."""
+ comment = tracker_pb2.IssueComment(
+ project_id=789, content='soon to be deleted', user_id=999,
+ issue_id=self.issue_1.issue_id)
+ self.services.issue.TestAddComment(comment, 1)
+
+ self.services.spam.FlagComment(
+ self.cnxn, self.issue_1, comment.id, 999, 111, True)
+ self.services.spam.RecordManualCommentVerdict(
+ self.cnxn, self.services.issue, self.services.user, comment.id, 111,
+ True)
+
+ request = issues_pb2.FlagCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ flag=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.FlagComment, mc, request)
+
+ comment_reports = self.services.spam.comment_reports_by_issue_id
+ manual_verdicts = self.services.spam.manual_verdicts_by_comment_id
+ self.assertEqual([], comment_reports[self.issue_1.issue_id][comment.id])
+ self.assertFalse(manual_verdicts[comment.id][111])
+
+ def testFlagComment_OwnerAutoVerdict(self):
+ """Test that an owner can flag a comment as spam and it is a verdict."""
+ comment = tracker_pb2.IssueComment(
+ project_id=789, content='soon to be deleted', user_id=999,
+ issue_id=self.issue_1.issue_id)
+ self.services.issue.TestAddComment(comment, 1)
+
+ request = issues_pb2.FlagCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.FlagComment, mc, request)
+
+ comment_reports = self.services.spam.comment_reports_by_issue_id
+ manual_verdicts = self.services.spam.manual_verdicts_by_comment_id
+ self.assertEqual([111], comment_reports[self.issue_1.issue_id][comment.id])
+ self.assertTrue(manual_verdicts[comment.id][111])
+
+ def testFlagComment_CommitterAutoVerdict(self):
+ """Test that an owner can flag an issue as spam and it is a verdict."""
+ self.services.user.TestAddUser('committer@example.com', 999)
+ self.services.project.TestAddProjectMembers(
+ [999], self.project, fake.COMMITTER_ROLE)
+
+ comment = tracker_pb2.IssueComment(
+ project_id=789, content='soon to be deleted', user_id=999,
+ issue_id=self.issue_1.issue_id)
+ self.services.issue.TestAddComment(comment, 1)
+
+ request = issues_pb2.FlagCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='committer@example.com')
+ self.CallWrapped(self.issues_svcr.FlagComment, mc, request)
+
+ comment_reports = self.services.spam.comment_reports_by_issue_id
+ manual_verdicts = self.services.spam.manual_verdicts_by_comment_id
+ self.assertEqual([999], comment_reports[self.issue_1.issue_id][comment.id])
+ self.assertTrue(manual_verdicts[comment.id][999])
+
+ def testFlagComment_ContributorAutoVerdict(self):
+ """Test that an owner can flag an issue as spam and it is a verdict."""
+ comment = tracker_pb2.IssueComment(
+ project_id=789, content='soon to be deleted', user_id=999,
+ issue_id=self.issue_1.issue_id)
+ self.services.issue.TestAddComment(comment, 1)
+
+ request = issues_pb2.FlagCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver2@example.com')
+ self.CallWrapped(self.issues_svcr.FlagComment, mc, request)
+
+ comment_reports = self.services.spam.comment_reports_by_issue_id
+ manual_verdicts = self.services.spam.manual_verdicts_by_comment_id
+ self.assertEqual([222], comment_reports[self.issue_1.issue_id][comment.id])
+ self.assertTrue(manual_verdicts[comment.id][222])
+
+ def testFlagComment_NotAllowed(self):
+ """Test that anon users cannot flag issues as spam."""
+ comment = tracker_pb2.IssueComment(
+ project_id=789, content='soon to be deleted', user_id=999,
+ issue_id=self.issue_1.issue_id)
+ self.services.issue.TestAddComment(comment, 1)
+
+ request = issues_pb2.FlagCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ flag=True)
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.FlagComment, mc, request)
+
+ comment_reports = self.services.spam.comment_reports_by_issue_id
+ manual_verdicts = self.services.spam.manual_verdicts_by_comment_id
+ self.assertNotIn(comment.id, comment_reports[self.issue_1.issue_id])
+ self.assertEqual({}, manual_verdicts[comment.id])
+
+ def testListIssuePermissions_Normal(self):
+ issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
+ self.services.issue.TestAddIssue(issue_1)
+
+ request = issues_pb2.ListIssuePermissionsRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+
+ response = self.CallWrapped(
+ self.issues_svcr.ListIssuePermissions, mc, request)
+ self.assertEqual(
+ issues_pb2.ListIssuePermissionsResponse(
+ permissions=[
+ 'addissuecomment',
+ 'createissue',
+ 'deleteown',
+ 'flagspam',
+ 'setstar',
+ 'view']),
+ response)
+
+ def testListIssuePermissions_DeletedIssue(self):
+ issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
+ issue_1.deleted = True
+ self.services.issue.TestAddIssue(issue_1)
+
+ request = issues_pb2.ListIssuePermissionsRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver2@example.com')
+
+ response = self.CallWrapped(
+ self.issues_svcr.ListIssuePermissions, mc, request)
+ self.assertEqual(
+ issues_pb2.ListIssuePermissionsResponse(permissions=['view']),
+ response)
+
+ def testListIssuePermissions_CanViewDeletedIssue(self):
+ issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
+ issue_1.deleted = True
+ self.services.issue.TestAddIssue(issue_1)
+
+ request = issues_pb2.ListIssuePermissionsRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ response = self.CallWrapped(
+ self.issues_svcr.ListIssuePermissions, mc, request)
+ self.assertEqual(
+ issues_pb2.ListIssuePermissionsResponse(permissions=[
+ 'deleteissue',
+ 'view']),
+ response)
+
+ def testListIssuePermissions_IssueRestrictions(self):
+ issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
+ issue_1.labels = ['Restrict-SetStar-CustomPerm']
+ self.services.issue.TestAddIssue(issue_1)
+
+ request = issues_pb2.ListIssuePermissionsRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver2@example.com')
+
+ response = self.CallWrapped(
+ self.issues_svcr.ListIssuePermissions, mc, request)
+ self.assertEqual(
+ issues_pb2.ListIssuePermissionsResponse(
+ permissions=[
+ 'addissuecomment',
+ 'createissue',
+ 'deleteown',
+ 'flagspam',
+ 'verdictspam',
+ 'view']),
+ response)
+
+ def testListIssuePermissions_IssueGrantedPerms(self):
+ self.services.config.CreateFieldDef(
+ self.cnxn, 789, 'Field Name', 'USER_TYPE', None, None, None, None, None,
+ None, None, None, None, None, 'CustomPerm', None, None, 'Docstring', [],
+ [])
+ issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
+ issue_1.labels = ['Restrict-SetStar-CustomPerm']
+ issue_1.field_values = [tracker_pb2.FieldValue(user_id=222, field_id=123)]
+ self.services.issue.TestAddIssue(issue_1)
+
+ request = issues_pb2.ListIssuePermissionsRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver2@example.com')
+
+ response = self.CallWrapped(
+ self.issues_svcr.ListIssuePermissions, mc, request)
+ self.assertEqual(
+ issues_pb2.ListIssuePermissionsResponse(
+ permissions=[
+ 'addissuecomment',
+ 'createissue',
+ 'customperm',
+ 'deleteown',
+ 'flagspam',
+ 'setstar',
+ 'verdictspam',
+ 'view']),
+ response)
+
+ @patch('services.tracker_fulltext.IndexIssues')
+ @patch('services.tracker_fulltext.UnindexIssues')
+ def testMoveIssue_Normal(self, _mock_index, _mock_unindex):
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
+ self.services.issue.TestAddIssue(issue)
+ self.project.owner_ids = [111]
+ target_project = self.services.project.TestAddProject(
+ 'dest', project_id=988, committer_ids=[111])
+
+ request = issues_pb2.MoveIssueRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ target_project_name='dest')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.MoveIssue, mc, request)
+
+ self.assertEqual(
+ issues_pb2.MoveIssueResponse(
+ new_issue_ref=common_pb2.IssueRef(
+ project_name='dest',
+ local_id=1)),
+ response)
+
+ moved_issue = self.services.issue.GetIssueByLocalID(self.cnxn,
+ target_project.project_id, 1)
+ self.assertEqual(target_project.project_id, moved_issue.project_id)
+ self.assertEqual(issue.summary, moved_issue.summary)
+ self.assertEqual(moved_issue.reporter_id, 111)
+
+ @patch('services.tracker_fulltext.IndexIssues')
+ def testCopyIssue_Normal(self, _mock_index):
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
+ self.services.issue.TestAddIssue(issue)
+ self.project.owner_ids = [111]
+
+ request = issues_pb2.CopyIssueRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ target_project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.CopyIssue, mc, request)
+
+ self.assertEqual(
+ issues_pb2.CopyIssueResponse(
+ new_issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=3)),
+ response)
+
+ copied_issue = self.services.issue.GetIssueByLocalID(self.cnxn,
+ self.project.project_id, 3)
+ self.assertEqual(self.project.project_id, copied_issue.project_id)
+ self.assertEqual(issue.summary, copied_issue.summary)
+ self.assertEqual(copied_issue.reporter_id, 111)
diff --git a/api/test/monorail_servicer_test.py b/api/test/monorail_servicer_test.py
new file mode 100644
index 0000000..8c5a1d3
--- /dev/null
+++ b/api/test/monorail_servicer_test.py
@@ -0,0 +1,484 @@
+# Copyright 2018 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 MonorailServicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import time
+import unittest
+import mock
+import mox
+
+from components.prpc import server
+from components.prpc import codes
+from components.prpc import context
+from google.appengine.ext import testbed
+from google.protobuf import json_format
+
+import settings
+from api import monorail_servicer
+from framework import authdata
+from framework import exceptions
+from framework import framework_constants
+from framework import monorailcontext
+from framework import permissions
+from framework import ratelimiter
+from framework import xsrf
+from services import cachemanager_svc
+from services import config_svc
+from services import service_manager
+from services import features_svc
+from testing import fake
+from testing import testing_helpers
+
+
+class MonorailServicerFunctionsTest(unittest.TestCase):
+
+ def testConvertPRPCStatusToHTTPStatus(self):
+ """We can convert pRPC status codes to http codes for monitoring."""
+ prpc_context = context.ServicerContext()
+
+ prpc_context.set_code(codes.StatusCode.OK)
+ self.assertEqual(
+ 200, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ self.assertEqual(
+ 400, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+ prpc_context.set_code(codes.StatusCode.PERMISSION_DENIED)
+ self.assertEqual(
+ 403, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+ prpc_context.set_code(codes.StatusCode.NOT_FOUND)
+ self.assertEqual(
+ 404, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+ prpc_context.set_code(codes.StatusCode.INTERNAL)
+ self.assertEqual(
+ 500, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+
+class UpdateSomethingRequest(testing_helpers.Blank):
+ """A fake request that would do a write."""
+ pass
+
+
+class ListSomethingRequest(testing_helpers.Blank):
+ """A fake request that would do a read."""
+ pass
+
+
+class TestableServicer(monorail_servicer.MonorailServicer):
+ """Fake servicer class."""
+
+ def __init__(self, services):
+ super(TestableServicer, self).__init__(services)
+ self.was_called = False
+ self.seen_mc = None
+ self.seen_request = None
+
+ @monorail_servicer.PRPCMethod
+ def CalcSomething(self, mc, request):
+ """Raise the test exception, or return what we got for verification."""
+ self.was_called = True
+ self.seen_mc = mc
+ self.seen_request = request
+ assert mc
+ assert request
+ if request.exc_class:
+ raise request.exc_class()
+ else:
+ return 'fake response proto'
+
+
+class MonorailServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.mox = mox.Mox()
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_datastore_v3_stub()
+ self.testbed.init_user_stub()
+
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ cache_manager=fake.CacheManager())
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=789, owner_ids=[111])
+ # allowlisted_bot's email is allowlisted in testing/api_clients.cfg.
+ self.allowlisted_bot = self.services.user.TestAddUser(
+ '123456789@developer.gserviceaccount.com', 999)
+ # allowlisted_client_id_user is used to test accounts that are only
+ # allowlisted with the client_id.
+ self.allowlisted_client_id_user = self.services.user.TestAddUser(
+ 'allowlisted-with-client-id@developer.gserviceaccount.com', 888)
+ self.non_member = self.services.user.TestAddUser(
+ 'nonmember@example.com', 222)
+ self.allowed_domain_user = self.services.user.TestAddUser(
+ 'chickenchicken@google.com', 333)
+ self.test_user = self.services.user.TestAddUser('test@example.com', 420)
+ self.svcr = TestableServicer(self.services)
+ self.nonmember_token = xsrf.GenerateToken(222, xsrf.XHR_SERVLET_PATH)
+ self.request = UpdateSomethingRequest(exc_class=None)
+ self.prpc_context = context.ServicerContext()
+ self.prpc_context.set_code(codes.StatusCode.OK)
+ self.prpc_context._invocation_metadata = [
+ (monorail_servicer.XSRF_TOKEN_HEADER, self.nonmember_token)]
+
+ self.oauth_patcher = mock.patch(
+ 'google.appengine.api.oauth.get_current_user')
+ self.mock_oauth_gcu = self.oauth_patcher.start()
+ self.mock_oauth_gcu.return_value = None
+
+ self.oauth_client_id_patcher = mock.patch(
+ 'google.appengine.api.oauth.get_client_id')
+ self.mock_oauth_gcid = self.oauth_client_id_patcher.start()
+ self.mock_oauth_gcid.return_value = "1234common.clientid"
+
+ # TODO(b/144508063): remove this workaround.
+ self.oauth_authorized_scopes_patcher = mock.patch(
+ 'google.appengine.api.oauth.get_authorized_scopes')
+ self.mock_oauth_gas = self.oauth_authorized_scopes_patcher.start()
+ self.mock_oauth_gas.return_value = [framework_constants.MONORAIL_SCOPE]
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+ self.testbed.deactivate()
+
+ def SetUpRecordMonitoringStats(self):
+ self.mox.StubOutWithMock(json_format, 'MessageToJson')
+ json_format.MessageToJson(self.request).AndReturn('json of request')
+ json_format.MessageToJson('fake response proto').AndReturn(
+ 'json of response')
+ self.mox.ReplayAll()
+
+ def testRun_SiteWide_Normal(self):
+ """Calling the handler through the decorator."""
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=unexpected-keyword-arg
+ response = self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
+ self.assertEqual(self.svcr.seen_mc.auth.email, self.non_member.email)
+ self.assertIn(permissions.CREATE_HOTLIST.lower(),
+ self.svcr.seen_mc.perms.perm_names)
+ self.assertNotIn(permissions.ADMINISTER_SITE.lower(),
+ self.svcr.seen_mc.perms.perm_names)
+ self.assertEqual(self.request, self.svcr.seen_request)
+ self.assertEqual('fake response proto', response)
+ self.assertEqual(codes.StatusCode.OK, self.prpc_context._code)
+
+ def testRun_RequesterBanned(self):
+ """If we reject the request, give PERMISSION_DENIED."""
+ self.non_member.banned = 'Spammer'
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=unexpected-keyword-arg
+ self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertFalse(self.svcr.was_called)
+ self.assertEqual(
+ codes.StatusCode.PERMISSION_DENIED, self.prpc_context._code)
+
+ def testRun_AnonymousRequester(self):
+ """Test we properly process anonymous users with valid tokens."""
+ self.prpc_context._invocation_metadata = [
+ (monorail_servicer.XSRF_TOKEN_HEADER,
+ xsrf.GenerateToken(0, xsrf.XHR_SERVLET_PATH))]
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=unexpected-keyword-arg
+ response = self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
+ self.assertIsNone(self.svcr.seen_mc.auth.email)
+ self.assertNotIn(permissions.CREATE_HOTLIST.lower(),
+ self.svcr.seen_mc.perms.perm_names)
+ self.assertNotIn(permissions.ADMINISTER_SITE.lower(),
+ self.svcr.seen_mc.perms.perm_names)
+ self.assertEqual(self.request, self.svcr.seen_request)
+ self.assertEqual('fake response proto', response)
+ self.assertEqual(codes.StatusCode.OK, self.prpc_context._code)
+
+ def testRun_DistributedInvalidation(self):
+ """The Run method must call DoDistributedInvalidation()."""
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=unexpected-keyword-arg
+ self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertIsNotNone(self.services.cache_manager.last_call)
+
+ def testRun_HandlerErrorResponse(self):
+ """An expected exception in the method causes an error status."""
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=attribute-defined-outside-init
+ self.request.exc_class = exceptions.NoSuchUserException
+ # pylint: disable=unexpected-keyword-arg
+ response = self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertTrue(self.svcr.was_called)
+ self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
+ self.assertEqual(self.svcr.seen_mc.auth.email, self.non_member.email)
+ self.assertEqual(self.request, self.svcr.seen_request)
+ self.assertIsNone(response)
+ self.assertEqual(codes.StatusCode.NOT_FOUND, self.prpc_context._code)
+
+ def testRun_HandlerProgrammingError(self):
+ """An unexception in the handler method is re-raised."""
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=attribute-defined-outside-init
+ self.request.exc_class = NotImplementedError
+ self.assertRaises(
+ NotImplementedError,
+ self.svcr.CalcSomething,
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertTrue(self.svcr.was_called)
+ self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
+
+ def testGetAndAssertRequesterAuth_Cookie_Anon(self):
+ """We get and allow requests from anon user using cookie auth."""
+ metadata = {
+ monorail_servicer.XSRF_TOKEN_HEADER: xsrf.GenerateToken(
+ 0, xsrf.XHR_SERVLET_PATH)}
+ # Signed out.
+ self.assertIsNone(self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services).email)
+
+ def testGetAndAssertRequesterAuth_Cookie_SignedIn(self):
+ """We get and allow requests from signed in users using cookie auth."""
+ metadata = dict(self.prpc_context.invocation_metadata())
+ # Signed in with cookie auth.
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ user_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual(self.non_member.email, user_auth.email)
+
+ def testGetAndAssertRequester_Anon_BadToken(self):
+ """We get the email address of the signed in user using oauth."""
+ metadata = {}
+ # Anonymous user has invalid token.
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetAndAssertRequester_Oauth_AllowedDomain_NoMonorailScope(self):
+ """We reject users with allowed domains but no monorail scope."""
+ metadata = {}
+ self.mock_oauth_gcu.return_value = None
+
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetAndAssertRequester_Oauth_BadDomain_MonorailScope(self):
+ """We reject users with bad domains using the monorail scope."""
+ metadata = {}
+ def side_effect(scope=None):
+ if scope == framework_constants.MONORAIL_SCOPE:
+ return testing_helpers.Blank(
+ email=lambda: 'testchicken@chicken.com', client_id=lambda: 7899)
+ return None
+ self.mock_oauth_gcu.side_effect = side_effect
+
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetAndAssertRequester_Oauth_AllowedDomain_MonorailScope(self):
+ """We get and allow users with allowed domains using the monorail scope."""
+ metadata = {}
+ def side_effect(scope=None):
+ if scope == framework_constants.MONORAIL_SCOPE:
+ return testing_helpers.Blank(
+ email=lambda: self.allowed_domain_user.email,
+ client_id=lambda: 7899)
+ return None
+ self.mock_oauth_gcu.side_effect = side_effect
+
+ user_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual(user_auth.email, self.allowed_domain_user.email)
+
+ def testGetAndAssertRequesterAuth_Oauth_Allowlisted(self):
+ metadata = {}
+ # Signed in with oauth.
+ self.mock_oauth_gcu.return_value = testing_helpers.Blank(
+ email=lambda: self.allowlisted_bot.email)
+
+ bot_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual(bot_auth.email, self.allowlisted_bot.email)
+
+ def testGetAndAssertRequesterAuth_Oauth_NotAllowlisted(self):
+ metadata = {}
+ # Signed in with oauth.
+ self.mock_oauth_gcu.return_value = testing_helpers.Blank(
+ email=lambda: 'who-is-this@test.com')
+
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetAndAssertRequesterAuth_Oauth_ClientIDOnly(self):
+ """We get and allow accounts that only have their client_id allowlisted."""
+ metadata = {}
+ self.mock_oauth_gcu.return_value = testing_helpers.Blank(
+ email=lambda: self.allowlisted_client_id_user.email)
+ self.mock_oauth_gcid.return_value = "98723764876"
+ both_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual(both_auth.email, self.allowlisted_client_id_user.email)
+
+ def testGetAndAssertRequesterAuth_Banned(self):
+ self.non_member.banned = 'Spammer'
+ metadata = dict(self.prpc_context.invocation_metadata())
+ # Signed in with cookie auth.
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ with self.assertRaises(permissions.BannedUserException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetRequester_TestAccountOnAppspot(self):
+ """Specifying test_account is ignored on deployed server."""
+ # pylint: disable=attribute-defined-outside-init
+ metadata = {'x-test-account': 'test@example.com'}
+ with self.assertRaises(exceptions.InputException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetRequester_TestAccountOnDev(self):
+ """For integration testing, we can set test_account on dev_server."""
+ try:
+ orig_local_mode = settings.local_mode
+ settings.local_mode = True
+
+ # pylint: disable=attribute-defined-outside-init
+ metadata = {'x-test-account': 'test@example.com'}
+ test_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual('test@example.com', test_auth.email)
+
+ # pylint: disable=attribute-defined-outside-init
+ metadata = {'x-test-account': 'test@anythingelse.com'}
+ with self.assertRaises(exceptions.InputException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+ finally:
+ settings.local_mode = orig_local_mode
+
+ def testAssertBaseChecks_SiteIsReadOnly_Write(self):
+ """We reject writes and allow reads when site is read-only."""
+ orig_read_only = settings.read_only
+ try:
+ settings.read_only = True
+ metadata = {}
+ self.assertRaises(
+ permissions.PermissionException,
+ self.svcr.AssertBaseChecks, self.request, metadata)
+ finally:
+ settings.read_only = orig_read_only
+
+ def testAssertBaseChecks_SiteIsReadOnly_Read(self):
+ """We reject writes and allow reads when site is read-only."""
+ orig_read_only = settings.read_only
+ try:
+ settings.read_only = True
+ metadata = {monorail_servicer.XSRF_TOKEN_HEADER: self.nonmember_token}
+
+ # Our default request is an update.
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.AssertBaseChecks(self.request, metadata)
+
+ # A method name starting with "List" or "Get" will run OK.
+ self.request = ListSomethingRequest(exc_class=None)
+ self.svcr.AssertBaseChecks(self.request, metadata)
+ finally:
+ settings.read_only = orig_read_only
+
+ def testGetRequestProject(self):
+ """We get a project specified by request field project_name."""
+ # No project specified.
+ self.assertIsNone(self.svcr.GetRequestProject(self.cnxn, self.request))
+
+ # Existing project specified.
+ # pylint: disable=attribute-defined-outside-init
+ self.request.project_name = 'proj'
+ self.assertEqual(
+ self.project, self.svcr.GetRequestProject(self.cnxn, self.request))
+
+ # Bad project specified.
+ # pylint: disable=attribute-defined-outside-init
+ self.request.project_name = 'not-a-proj'
+ self.assertIsNone(self.svcr.GetRequestProject(self.cnxn, self.request))
+
+ def CheckExceptionStatus(self, e, expected_code, details=None):
+ mc = monorailcontext.MonorailContext(self.services)
+ self.prpc_context.set_code(codes.StatusCode.OK)
+ processed = self.svcr.ProcessException(e, self.prpc_context, mc)
+ if expected_code:
+ self.assertTrue(processed)
+ self.assertEqual(expected_code, self.prpc_context._code)
+ else:
+ self.assertFalse(processed)
+ # Uncaught exceptions should indicate an error.
+ self.assertEqual(codes.StatusCode.INTERNAL, self.prpc_context._code)
+ if details is not None:
+ self.assertEqual(details, self.prpc_context._details)
+
+ def testProcessException(self):
+ """Expected exceptions are converted to pRPC codes, expected not."""
+ self.CheckExceptionStatus(
+ exceptions.NoSuchUserException(), codes.StatusCode.NOT_FOUND)
+ self.CheckExceptionStatus(
+ exceptions.NoSuchProjectException(), codes.StatusCode.NOT_FOUND)
+ self.CheckExceptionStatus(
+ exceptions.NoSuchIssueException(), codes.StatusCode.NOT_FOUND)
+ self.CheckExceptionStatus(
+ exceptions.NoSuchComponentException(), codes.StatusCode.NOT_FOUND)
+ self.CheckExceptionStatus(
+ permissions.BannedUserException(), codes.StatusCode.PERMISSION_DENIED)
+ self.CheckExceptionStatus(
+ permissions.PermissionException(), codes.StatusCode.PERMISSION_DENIED)
+ self.CheckExceptionStatus(
+ exceptions.GroupExistsException(), codes.StatusCode.ALREADY_EXISTS)
+ self.CheckExceptionStatus(
+ exceptions.InvalidComponentNameException(),
+ codes.StatusCode.INVALID_ARGUMENT)
+ self.CheckExceptionStatus(
+ exceptions.InputException('echoed values'),
+ codes.StatusCode.INVALID_ARGUMENT,
+ details='Invalid arguments: echoed values')
+ self.CheckExceptionStatus(
+ exceptions.FilterRuleException(),
+ codes.StatusCode.INVALID_ARGUMENT,
+ details='Violates filter rule that should error.')
+ self.CheckExceptionStatus(
+ ratelimiter.ApiRateLimitExceeded('client_id', 'email'),
+ codes.StatusCode.PERMISSION_DENIED)
+ self.CheckExceptionStatus(
+ features_svc.HotlistAlreadyExists(), codes.StatusCode.ALREADY_EXISTS)
+ self.CheckExceptionStatus(NotImplementedError(), None)
+
+ def testProcessException_ErrorMessageEscaped(self):
+ """If we ever echo user input in error messages, it is escaped.."""
+ self.CheckExceptionStatus(
+ exceptions.InputException('echoed <script>"code"</script>'),
+ codes.StatusCode.INVALID_ARGUMENT,
+ details=('Invalid arguments: echoed '
+ '<script>"code"</script>'))
+
+ def testRecordMonitoringStats_RequestClassDoesNotEndInRequest(self):
+ """We cope with request proto class names that do not end in 'Request'."""
+ self.request = 'this is a string'
+ self.SetUpRecordMonitoringStats()
+ start_time = 1522559788.939511
+ now = 1522569311.892738
+ self.svcr.RecordMonitoringStats(
+ start_time, self.request, 'fake response proto', self.prpc_context,
+ now=now)
diff --git a/api/test/projects_servicer_test.py b/api/test/projects_servicer_test.py
new file mode 100644
index 0000000..b3084c3
--- /dev/null
+++ b/api/test/projects_servicer_test.py
@@ -0,0 +1,1086 @@
+# Copyright 2018 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 projects servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+from mock import patch
+
+from components.prpc import codes
+from components.prpc import context
+from components.prpc import server
+
+from api import projects_servicer
+from api.api_proto import common_pb2
+from api.api_proto import issue_objects_pb2
+from api.api_proto import project_objects_pb2
+from api.api_proto import projects_pb2
+from framework import authdata
+from framework import exceptions
+from framework import framework_constants
+from framework import monorailcontext
+from framework import permissions
+from proto import tracker_pb2
+from proto import project_pb2
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+from testing import fake
+from testing import testing_helpers
+from services import service_manager
+
+
+class ProjectsServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ project_star=fake.ProjectStarService(),
+ features=fake.FeaturesService())
+
+ self.admin = self.services.user.TestAddUser('admin@example.com', 123)
+ self.admin.is_site_admin = True
+ self.owner = self.services.user.TestAddUser('owner@example.com', 111)
+ self.services.user.TestAddUser('user_222@example.com', 222)
+ self.services.user.TestAddUser('user_333@example.com', 333)
+ self.services.user.TestAddUser('user_444@example.com', 444)
+ self.services.user.TestAddUser('user_666@example.com', 666)
+
+ # User group 888 has members: user_555 and proj@monorail.com
+ self.services.user.TestAddUser('group888@googlegroups.com', 888)
+ self.services.usergroup.TestAddGroupSettings(
+ 888, 'group888@googlegroups.com')
+ self.services.usergroup.TestAddMembers(888, [555, 1001])
+
+ # User group 999 has members: user_111 and user_444
+ self.services.user.TestAddUser('group999@googlegroups.com', 999)
+ self.services.usergroup.TestAddGroupSettings(
+ 999, 'group999@googlegroups.com')
+ self.services.usergroup.TestAddMembers(999, [111, 444])
+
+ # User group 777 has members: user_666 and group 999.
+ self.services.user.TestAddUser('group777@googlegroups.com', 777)
+ self.services.usergroup.TestAddGroupSettings(
+ 777, 'group777@googlegroups.com')
+ self.services.usergroup.TestAddMembers(777, [666, 999])
+
+ self.project = self.services.project.TestAddProject(
+ 'proj',
+ project_id=789,
+ owner_ids=[111],
+ committer_ids=[222],
+ contrib_ids=[333])
+ self.projects_svcr = projects_servicer.ProjectsServicer(
+ self.services, make_rate_limiter=False)
+ self.prpc_context = context.ServicerContext()
+ self.prpc_context.set_code(codes.StatusCode.OK)
+
+ def CallWrapped(self, wrapped_handler, *args, **kwargs):
+ return wrapped_handler.wrapped(self.projects_svcr, *args, **kwargs)
+
+ def testListProjects_Normal(self):
+ """We can get a list of all projects on the site."""
+ request = projects_pb2.ListProjectsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.projects_svcr.ListProjects, mc, request)
+ self.assertEqual(2, len(response.projects))
+
+ def testGetConfig_Normal(self):
+ """We can get a project config."""
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.projects_svcr.GetConfig, mc, request)
+ self.assertEqual('proj', response.project_name)
+
+ def testGetConfig_NoSuchProject(self):
+ """We reject a request to get a config for a non-existent project."""
+ request = projects_pb2.GetConfigRequest(project_name='unknown-proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ self.CallWrapped(self.projects_svcr.GetConfig, mc, request)
+
+ def testGetConfig_PermissionDenied(self):
+ """We reject a request to get a config for a non-viewable project."""
+ self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+
+ # User is a member of the members-only project.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.projects_svcr.GetConfig, mc, request)
+ self.assertEqual('proj', response.project_name)
+
+ # User is not a member of the members-only project.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.projects_svcr.GetConfig, mc, request)
+
+ @patch('businesslogic.work_env.WorkEnv.ListProjectTemplates')
+ def testListProjectTemplates_Normal(self, mockListProjectTemplates):
+ fd_1 = tracker_pb2.FieldDef(
+ field_name='FirstField', field_id=1,
+ field_type=tracker_pb2.FieldTypes.STR_TYPE)
+ fd_2 = tracker_pb2.FieldDef(
+ field_name='LegalApproval', field_id=2,
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE)
+ component = tracker_pb2.ComponentDef(component_id=1, path='dude')
+ status_def = tracker_pb2.StatusDef(status='New', means_open=True)
+ config = tracker_pb2.ProjectIssueConfig(
+ project_id=789, field_defs=[fd_1, fd_2], component_defs=[component],
+ well_known_statuses=[status_def])
+ self.services.config.StoreConfig(self.cnxn, config)
+ admin1 = self.services.user.TestAddUser('admin@example.com', 222)
+ appr1 = self.services.user.TestAddUser('approver@example.com', 333)
+ setter = self.services.user.TestAddUser('setter@example.com', 444)
+ template = tracker_pb2.TemplateDef(
+ name='Chicken', content='description', summary='summary',
+ status='New', admin_ids=[admin1.user_id],
+ field_values=[tracker_bizobj.MakeFieldValue(
+ fd_1.field_id, None, 'Cow', None, None, None, False)],
+ component_ids=[component.component_id],
+ approval_values=[tracker_pb2.ApprovalValue(
+ approval_id=2, approver_ids=[appr1.user_id],
+ setter_id=setter.user_id)])
+ mockListProjectTemplates.return_value = [template]
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ request = projects_pb2.ListProjectTemplatesRequest(project_name='proj')
+ response = self.CallWrapped(
+ self.projects_svcr.ListProjectTemplates, mc, request)
+ self.assertEqual(
+ response,
+ projects_pb2.ListProjectTemplatesResponse(
+ templates=[project_objects_pb2.TemplateDef(
+ template_name='Chicken',
+ content='description',
+ summary='summary',
+ status_ref=common_pb2.StatusRef(
+ status='New',
+ is_derived=False,
+ means_open=True),
+ owner_defaults_to_member=True,
+ admin_refs=[
+ common_pb2.UserRef(
+ user_id=admin1.user_id,
+ display_name=testing_helpers.ObscuredEmail(admin1.email),
+ is_derived=False)],
+ field_values=[
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=fd_1.field_id,
+ field_name=fd_1.field_name,
+ type=common_pb2.STR_TYPE),
+ value='Cow')],
+ component_refs=[
+ common_pb2.ComponentRef(
+ path=component.path, is_derived=False)],
+ approval_values=[
+ issue_objects_pb2.Approval(
+ field_ref=common_pb2.FieldRef(
+ field_id=fd_2.field_id,
+ field_name=fd_2.field_name,
+ type=common_pb2.APPROVAL_TYPE),
+ setter_ref=common_pb2.UserRef(
+ user_id=setter.user_id,
+ display_name=testing_helpers.ObscuredEmail(
+ setter.email)),
+ phase_ref=issue_objects_pb2.PhaseRef(),
+ approver_refs=[common_pb2.UserRef(
+ user_id=appr1.user_id,
+ display_name=testing_helpers.ObscuredEmail(appr1.email),
+ is_derived=False)])],
+ )]))
+
+ def testListProjectTemplates_NoProjectName(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ request = projects_pb2.ListProjectTemplatesRequest()
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.projects_svcr.ListProjectTemplates, mc, request)
+
+ def testListProjectTemplates_NoSuchProject(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ request = projects_pb2.ListProjectTemplatesRequest(project_name='ghost')
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ self.CallWrapped(self.projects_svcr.ListProjectTemplates, mc, request)
+
+ def testListProjectTemplates_PermissionDenied(self):
+ self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.projects_svcr.ListProjectTemplates, mc, request)
+
+ def testGetPresentationConfig_Normal(self):
+ """Test getting project summary, thumbnail url, custom issue entry, etc."""
+ config = tracker_pb2.ProjectIssueConfig(project_id=789)
+ self.project.summary = 'project summary'
+ config.custom_issue_entry_url = 'issue entry url'
+ config.member_default_query = 'default query'
+ config.default_col_spec = 'ID Summary'
+ config.default_sort_spec = 'Priority Status'
+ config.default_x_attr = 'Priority'
+ config.default_y_attr = 'Status'
+ self.project.revision_url_format = 'revision url format'
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ request = projects_pb2.GetPresentationConfigRequest(project_name='proj')
+ response = self.CallWrapped(
+ self.projects_svcr.GetPresentationConfig, mc, request)
+
+ self.assertEqual('project summary', response.project_summary)
+ self.assertEqual('issue entry url', response.custom_issue_entry_url)
+ self.assertEqual('default query', response.default_query)
+ self.assertEqual('ID Summary', response.default_col_spec)
+ self.assertEqual('Priority Status', response.default_sort_spec)
+ self.assertEqual('Priority', response.default_x_attr)
+ self.assertEqual('Status', response.default_y_attr)
+ self.assertEqual('revision url format', response.revision_url_format)
+
+ def testGetPresentationConfig_SavedQueriesAllowed(self):
+ """Only project members or higher can see project saved queries."""
+ self.services.features.UpdateCannedQueries(self.cnxn, 789, [
+ tracker_pb2.SavedQuery(query_id=101, name='test', query='owner:me'),
+ tracker_pb2.SavedQuery(query_id=202, name='hello', query='world')
+ ])
+
+ # User 333 is a contributor.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user_333@example.com')
+
+ request = projects_pb2.GetPresentationConfigRequest(project_name='proj')
+ response = self.CallWrapped(self.projects_svcr.GetPresentationConfig, mc,
+ request)
+
+ self.assertEqual(2, len(response.saved_queries))
+
+ self.assertEqual(101, response.saved_queries[0].query_id)
+ self.assertEqual('test', response.saved_queries[0].name)
+ self.assertEqual('owner:me', response.saved_queries[0].query)
+
+ self.assertEqual(202, response.saved_queries[1].query_id)
+ self.assertEqual('hello', response.saved_queries[1].name)
+ self.assertEqual('world', response.saved_queries[1].query)
+
+ def testGetPresentationConfig_SavedQueriesDenied(self):
+ """Only project members or higher can see project saved queries."""
+ self.services.features.UpdateCannedQueries(self.cnxn, 789, [
+ tracker_pb2.SavedQuery(query_id=101, name='test', query='owner:me'),
+ tracker_pb2.SavedQuery(query_id=202, name='hello', query='world')
+ ])
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+
+ request = projects_pb2.GetPresentationConfigRequest(project_name='proj')
+ response = self.CallWrapped(self.projects_svcr.GetPresentationConfig, mc,
+ request)
+
+ self.assertEqual(0, len(response.saved_queries))
+
+ def testGetCustomPermissions_Normal(self):
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=111,
+ perms=['FooPerm', 'BarPerm'])]
+
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.org')
+ response = self.CallWrapped(
+ self.projects_svcr.GetCustomPermissions, mc, request)
+ self.assertEqual(['BarPerm', 'FooPerm'], response.permissions)
+
+ def testGetCustomPermissions_PermissionsAreDedupped(self):
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=111,
+ perms=['FooPerm', 'FooPerm']),
+ project_pb2.Project.ExtraPerms(
+ member_id=222,
+ perms=['FooPerm'])]
+
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.org')
+ response = self.CallWrapped(
+ self.projects_svcr.GetCustomPermissions, mc, request)
+ self.assertEqual(['FooPerm'], response.permissions)
+
+ def testGetCustomPermissions_PermissionsAreSorted(self):
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=111,
+ perms=['FooPerm', 'BarPerm']),
+ project_pb2.Project.ExtraPerms(
+ member_id=222,
+ perms=['BazPerm'])]
+
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.org')
+ response = self.CallWrapped(
+ self.projects_svcr.GetCustomPermissions, mc, request)
+ self.assertEqual(['BarPerm', 'BazPerm', 'FooPerm'], response.permissions)
+
+ def testGetCustomPermissions_IgnoreStandardPermissions(self):
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=111,
+ perms=permissions.STANDARD_PERMISSIONS + ['FooPerm'])]
+
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.org')
+ response = self.CallWrapped(
+ self.projects_svcr.GetCustomPermissions, mc, request)
+ self.assertEqual(['FooPerm'], response.permissions)
+
+ def testGetCustomPermissions_NoCustomPermissions(self):
+ self.project.extra_perms = []
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.org')
+ response = self.CallWrapped(
+ self.projects_svcr.GetCustomPermissions, mc, request)
+ self.assertEqual([], response.permissions)
+
+ def assertVisibleMembers(self, expected_user_ids, expected_group_ids,
+ requester=None):
+ request = projects_pb2.GetVisibleMembersRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=requester)
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.GetVisibleMembers, mc, request)
+ self.assertEqual(
+ expected_user_ids,
+ [user_ref.user_id for user_ref in response.user_refs])
+ # Assert that we get the full email address.
+ self.assertEqual(
+ [self.services.user.LookupUserEmail(self.cnxn, user_id)
+ for user_id in expected_user_ids],
+ [user_ref.display_name for user_ref in response.user_refs])
+ self.assertEqual(
+ expected_group_ids,
+ [group_ref.user_id for group_ref in response.group_refs])
+ # Assert that we get the full email address.
+ self.assertEqual(
+ [self.services.user.LookupUserEmail(self.cnxn, user_id)
+ for user_id in expected_group_ids],
+ [group_ref.display_name for group_ref in response.group_refs])
+ return response
+
+ def testGetVisibleMembers_Normal(self):
+ # Not logged in - Test users have their email addresses obscured to
+ # non-project members by default.
+ self.assertVisibleMembers([], [])
+ # Logged in as non project member
+ self.assertVisibleMembers([], [], requester='foo@example.com')
+ # Logged in as owner
+ self.assertVisibleMembers([111, 222, 333], [],
+ requester='owner@example.com')
+ # Logged in as committer
+ self.assertVisibleMembers([111, 222, 333], [],
+ requester='user_222@example.com')
+ # Logged in as contributor
+ self.assertVisibleMembers([111, 222, 333], [],
+ requester='user_333@example.com')
+
+ def testGetVisibleMembers_OnlyOwnersSeeContributors(self):
+ self.project.only_owners_see_contributors = True
+ # Not logged in
+ with self.assertRaises(permissions.PermissionException):
+ self.assertVisibleMembers([111, 222], [])
+ # Logged in with a non-member
+ with self.assertRaises(permissions.PermissionException):
+ self.assertVisibleMembers([111, 222], [], requester='foo@example.com')
+ # Logged in as owner
+ self.assertVisibleMembers([111, 222, 333], [],
+ requester='owner@example.com')
+ # Logged in as committer
+ self.assertVisibleMembers([111, 222, 333], [],
+ requester='user_222@example.com')
+ # Logged in as contributor
+ with self.assertRaises(permissions.PermissionException):
+ self.assertVisibleMembers(
+ [111, 222], [], requester='user_333@example.com')
+
+ def testGetVisibleMembers_MemberIsGroup(self):
+ self.project.contributor_ids.extend([999])
+ self.assertVisibleMembers([999, 111, 222, 333, 444], [999],
+ requester='owner@example.com')
+
+ def testGetVisibleMembers_AcExclusion(self):
+ self.services.project.ac_exclusion_ids[self.project.project_id] = [333]
+ self.assertVisibleMembers([111, 222], [], requester='owner@example.com')
+
+ def testGetVisibleMembers_NoExpand(self):
+ self.services.project.no_expand_ids[self.project.project_id] = [999]
+ self.project.contributor_ids.extend([999])
+ self.assertVisibleMembers([999, 111, 222, 333], [999],
+ requester='owner@example.com')
+
+ def testGetVisibleMembers_ObscuredEmails(self):
+ # Unobscure the owner's email. Non-project members can see.
+ self.services.user.UpdateUserSettings(
+ self.cnxn, 111, self.owner, obscure_email=False)
+
+ # Not logged in
+ self.assertVisibleMembers([111], [])
+ # Logged in as not a project member
+ self.assertVisibleMembers([111], [], requester='foo@example.com')
+ # Logged in as owner
+ self.assertVisibleMembers(
+ [111, 222, 333], [], requester='owner@example.com')
+ # Logged in as committer
+ self.assertVisibleMembers(
+ [111, 222, 333], [], requester='user_222@example.com')
+ # Logged in as contributor
+ self.assertVisibleMembers(
+ [111, 222, 333], [], requester='user_333@example.com')
+
+ def testListStatuses(self):
+ request = projects_pb2.ListStatusesRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListStatuses, mc, request)
+ self.assertFalse(response.restrict_to_known)
+ self.assertEqual(
+ [('New', True),
+ ('Accepted', True),
+ ('Started', True),
+ ('Fixed', False),
+ ('Verified', False),
+ ('Invalid', False),
+ ('Duplicate', False),
+ ('WontFix', False),
+ ('Done', False)],
+ [(status_def.status, status_def.means_open)
+ for status_def in response.status_defs])
+ self.assertEqual(
+ [('Duplicate', False)],
+ [(status_def.status, status_def.means_open)
+ for status_def in response.statuses_offer_merge])
+
+ def testListComponents(self):
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Foo', 'Foo Component', True, [],
+ [], True, 111, [])
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Bar', 'Bar Component', False, [],
+ [], True, 111, [])
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Bar>Baz', 'Baz Component',
+ False, [], [], True, 111, [])
+
+ request = projects_pb2.ListComponentsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListComponents, mc, request)
+
+ self.assertEqual(
+ [project_objects_pb2.ComponentDef(
+ path='Foo',
+ docstring='Foo Component',
+ deprecated=True),
+ project_objects_pb2.ComponentDef(
+ path='Bar',
+ docstring='Bar Component',
+ deprecated=False),
+ project_objects_pb2.ComponentDef(
+ path='Bar>Baz',
+ docstring='Baz Component',
+ deprecated=False)],
+ list(response.component_defs))
+
+ def testListComponents_IncludeAdminInfo(self):
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Foo', 'Foo Component', True, [],
+ [], 1234567, 111, [])
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Bar', 'Bar Component', False, [],
+ [], 1234568, 111, [])
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Bar>Baz', 'Baz Component',
+ False, [], [], 1234569, 111, [])
+ creator_ref = common_pb2.UserRef(
+ user_id=111,
+ display_name='owner@example.com')
+
+ request = projects_pb2.ListComponentsRequest(
+ project_name='proj', include_admin_info=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListComponents, mc, request)
+
+ self.assertEqual(
+ [project_objects_pb2.ComponentDef(
+ path='Foo',
+ docstring='Foo Component',
+ deprecated=True,
+ created=1234567,
+ creator_ref=creator_ref),
+ project_objects_pb2.ComponentDef(
+ path='Bar',
+ docstring='Bar Component',
+ deprecated=False,
+ created=1234568,
+ creator_ref=creator_ref),
+ project_objects_pb2.ComponentDef(
+ path='Bar>Baz',
+ docstring='Baz Component',
+ deprecated=False,
+ created=1234569,
+ creator_ref=creator_ref),
+ ],
+ list(response.component_defs))
+
+ def AddField(self, name, **kwargs):
+ if kwargs.get('needs_perm'):
+ kwargs['needs_member'] = True
+ kwargs.setdefault('cnxn', self.cnxn)
+ kwargs.setdefault('project_id', self.project.project_id)
+ kwargs.setdefault('field_name', name)
+ kwargs.setdefault('field_type_str', 'USER_TYPE')
+ for arg in ('applic_type', 'applic_pred', 'is_required', 'is_niche',
+ 'is_multivalued', 'min_value', 'max_value', 'regex',
+ 'needs_member', 'needs_perm', 'grants_perm', 'notify_on',
+ 'date_action_str', 'docstring'):
+ kwargs.setdefault(arg, None)
+ for arg in ('admin_ids', 'editor_ids'):
+ kwargs.setdefault(arg, [])
+
+ self.services.config.CreateFieldDef(**kwargs)
+
+ def testListFields_Normal(self):
+ self.AddField('Foo Field', needs_perm=permissions.EDIT_ISSUE)
+
+ request = projects_pb2.ListFieldsRequest(
+ project_name='proj', include_user_choices=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual('Foo Field', field.field_ref.field_name)
+ self.assertEqual(
+ [111, 222],
+ sorted([user_ref.user_id for user_ref in field.user_choices]))
+ self.assertEqual(
+ ['owner@example.com', 'user_222@example.com'],
+ sorted([user_ref.display_name for user_ref in field.user_choices]))
+
+ def testListFields_DontIncludeUserChoices(self):
+ self.AddField('Foo Field', needs_perm=permissions.EDIT_ISSUE)
+
+ request = projects_pb2.ListFieldsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual(0, len(field.user_choices))
+
+ def testListFields_IncludeAdminInfo(self):
+ self.AddField('Foo Field', needs_perm=permissions.EDIT_ISSUE, is_niche=True,
+ applic_type='Foo Applic Type')
+
+ request = projects_pb2.ListFieldsRequest(
+ project_name='proj', include_admin_info=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual('Foo Field', field.field_ref.field_name)
+ self.assertEqual(True, field.is_niche)
+ self.assertEqual('Foo Applic Type', field.applicable_type)
+
+ def testListFields_EnumFieldChoices(self):
+ self.AddField('Type', field_type_str='ENUM_TYPE')
+
+ request = projects_pb2.ListFieldsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual('Type', field.field_ref.field_name)
+ self.assertEqual(
+ ['Defect', 'Enhancement', 'Task', 'Other'],
+ [label.label for label in field.enum_choices])
+
+ def testListFields_CustomPermission(self):
+ self.AddField('Foo Field', needs_perm='FooPerm')
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=111,
+ perms=['UnrelatedPerm']),
+ project_pb2.Project.ExtraPerms(
+ member_id=222,
+ perms=['FooPerm'])]
+
+ request = projects_pb2.ListFieldsRequest(
+ project_name='proj', include_user_choices=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual('Foo Field', field.field_ref.field_name)
+ self.assertEqual(
+ [222],
+ sorted([user_ref.user_id for user_ref in field.user_choices]))
+ self.assertEqual(
+ ['user_222@example.com'],
+ sorted([user_ref.display_name for user_ref in field.user_choices]))
+
+ def testListFields_IndirectPermission(self):
+ """Test that the permissions of effective ids are also considered."""
+ self.AddField('Foo Field', needs_perm='FooPerm')
+ self.project.contributor_ids.extend([999])
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=999,
+ perms=['FooPerm', 'BarPerm'])]
+
+ request = projects_pb2.ListFieldsRequest(
+ project_name='proj', include_user_choices=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual('Foo Field', field.field_ref.field_name)
+ # Users 111 and 444 are members of group 999, which has the needed
+ # permission.
+ self.assertEqual(
+ [111, 444, 999],
+ sorted([user_ref.user_id for user_ref in field.user_choices]))
+ self.assertEqual(
+ ['group999@googlegroups.com', 'owner@example.com',
+ 'user_444@example.com'],
+ sorted([user_ref.display_name for user_ref in field.user_choices]))
+
+ def testListFields_TwiceIndirectPermission(self):
+ """Test that only direct memberships are considered."""
+ self.AddField('Foo Field', needs_perm='FooPerm')
+ # User group 777 has members: user_666 and group 999.
+ self.project.contributor_ids.extend([777])
+ self.project.contributor_ids.extend([999])
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=777, perms=['FooPerm', 'BarPerm'])
+ ]
+
+ request = projects_pb2.ListFieldsRequest(
+ project_name='proj', include_user_choices=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual('Foo Field', field.field_ref.field_name)
+ self.assertEqual(
+ [666, 777, 999],
+ sorted([user_ref.user_id for user_ref in field.user_choices]))
+ self.assertEqual(
+ [
+ 'group777@googlegroups.com', 'group999@googlegroups.com',
+ 'user_666@example.com'
+ ], sorted([user_ref.display_name for user_ref in field.user_choices]))
+
+ def testListFields_NoPermissionsNeeded(self):
+ self.AddField('Foo Field')
+
+ request = projects_pb2.ListFieldsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual('Foo Field', field.field_ref.field_name)
+
+ def testListFields_MultipleFields(self):
+ self.AddField('Bar Field', needs_perm=permissions.VIEW)
+ self.AddField('Foo Field', needs_perm=permissions.EDIT_ISSUE)
+
+ request = projects_pb2.ListFieldsRequest(
+ project_name='proj', include_user_choices=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(2, len(response.field_defs))
+ field_defs = sorted(
+ response.field_defs, key=lambda field: field.field_ref.field_name)
+
+ self.assertEqual(
+ ['Bar Field', 'Foo Field'],
+ [field.field_ref.field_name for field in field_defs])
+ self.assertEqual(
+ [[111, 222, 333],
+ [111, 222]],
+ [sorted(user_ref.user_id for user_ref in field.user_choices)
+ for field in field_defs])
+ self.assertEqual(
+ [['owner@example.com', 'user_222@example.com', 'user_333@example.com'],
+ ['owner@example.com', 'user_222@example.com']],
+ [sorted(user_ref.display_name for user_ref in field.user_choices)
+ for field in field_defs])
+
+ def testListFields_NoFields(self):
+ request = projects_pb2.ListFieldsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(0, len(response.field_defs))
+
+ def testGetLabelOptions_Normal(self):
+ request = projects_pb2.GetLabelOptionsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.GetLabelOptions, mc, request)
+
+ expected_label_names = [
+ label[0] for label in tracker_constants.DEFAULT_WELL_KNOWN_LABELS]
+ expected_label_names += [
+ 'Restrict-View-EditIssue', 'Restrict-AddIssueComment-EditIssue',
+ 'Restrict-View-CoreTeam']
+ self.assertEqual(
+ sorted(expected_label_names),
+ sorted(label.label for label in response.label_options))
+
+ def testGetLabelOptions_CustomPermissions(self):
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=222,
+ perms=['FooPerm', 'BarPerm'])]
+
+ request = projects_pb2.GetLabelOptionsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.GetLabelOptions, mc, request)
+
+ expected_label_names = [
+ label[0] for label in tracker_constants.DEFAULT_WELL_KNOWN_LABELS]
+ expected_label_names += [
+ 'Restrict-View-EditIssue', 'Restrict-AddIssueComment-EditIssue']
+ expected_label_names += [
+ 'Restrict-%s-%s' % (std_perm, custom_perm)
+ for std_perm in permissions.STANDARD_ISSUE_PERMISSIONS
+ for custom_perm in ('BarPerm', 'FooPerm')]
+
+ self.assertEqual(
+ sorted(expected_label_names),
+ sorted(label.label for label in response.label_options))
+
+ def testGetLabelOptions_FieldMasksLabel(self):
+ self.AddField('Type', field_type_str='ENUM_TYPE')
+
+ request = projects_pb2.GetLabelOptionsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.GetLabelOptions, mc, request)
+
+ expected_label_names = [
+ label[0] for label in tracker_constants.DEFAULT_WELL_KNOWN_LABELS
+ if not label[0].startswith('Type-')
+ ]
+ expected_label_names += [
+ 'Restrict-View-EditIssue', 'Restrict-AddIssueComment-EditIssue',
+ 'Restrict-View-CoreTeam']
+ self.assertEqual(
+ sorted(expected_label_names),
+ sorted(label.label for label in response.label_options))
+
+ def CallGetStarCount(self):
+ request = projects_pb2.GetProjectStarCountRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.GetProjectStarCount, mc, request)
+ return response.star_count
+
+ def CallStar(self, requester='owner@example.com', starred=True):
+ request = projects_pb2.StarProjectRequest(
+ project_name='proj', starred=starred)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=requester)
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.StarProject, mc, request)
+ return response.star_count
+
+ def testStarCount_Normal(self):
+ self.assertEqual(0, self.CallGetStarCount())
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ def testStarCount_StarTwiceSameUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ def testStarCount_StarTwiceDifferentUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(2, self.CallStar(requester='user_222@example.com'))
+ self.assertEqual(2, self.CallGetStarCount())
+
+ def testStarCount_RemoveStarTwiceSameUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ self.assertEqual(0, self.CallStar(starred=False))
+ self.assertEqual(0, self.CallStar(starred=False))
+ self.assertEqual(0, self.CallGetStarCount())
+
+ def testStarCount_RemoveStarTwiceDifferentUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(2, self.CallStar(requester='user_222@example.com'))
+ self.assertEqual(2, self.CallGetStarCount())
+
+ self.assertEqual(1, self.CallStar(starred=False))
+ self.assertEqual(
+ 0, self.CallStar(requester='user_222@example.com', starred=False))
+ self.assertEqual(0, self.CallGetStarCount())
+
+ def testCheckProjectName_OK(self):
+ """We can check a project name."""
+ request = projects_pb2.CheckProjectNameRequest(project_name='foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.CheckProjectName, mc, request)
+
+ self.assertEqual('', response.error)
+
+ def testCheckProjectName_InvalidProjectName(self):
+ """We reject an invalid project name."""
+ request = projects_pb2.CheckProjectNameRequest(project_name='Foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.CheckProjectName, mc, request)
+
+ self.assertNotEqual('', response.error)
+
+ def testCheckProjectName_NotAllowed(self):
+ """Users that can't create a project shouldn't get any information."""
+ request = projects_pb2.CheckProjectNameRequest(project_name='Foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.projects_svcr.CheckProjectName, mc, request)
+
+ def testCheckProjectName_ProjectAlreadyExists(self):
+ """There is already a project with that name."""
+ request = projects_pb2.CheckProjectNameRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.CheckProjectName, mc, request)
+
+ self.assertNotEqual('', response.error)
+
+ def testCheckComponentName_OK(self):
+ request = projects_pb2.CheckComponentNameRequest(
+ project_name='proj',
+ component_name='Component')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.CheckComponentName, mc, request)
+
+ self.assertEqual('', response.error)
+
+ def testCheckComponentName_ParentComponentOK(self):
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Component', 'Docstring',
+ False, [], [], 0, 111, [])
+ request = projects_pb2.CheckComponentNameRequest(
+ project_name='proj',
+ parent_path='Component',
+ component_name='Path')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.CheckComponentName, mc, request)
+
+ self.assertEqual('', response.error)
+
+ def testCheckComponentName_InvalidComponentName(self):
+ request = projects_pb2.CheckComponentNameRequest(
+ project_name='proj',
+ component_name='Component-')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.CheckComponentName, mc, request)
+
+ self.assertNotEqual('', response.error)
+
+ def testCheckComponentName_ComponentAlreadyExists(self):
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Component', 'Docstring',
+ False, [], [], 0, 111, [])
+ request = projects_pb2.CheckComponentNameRequest(
+ project_name='proj',
+ component_name='Component')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.CheckComponentName, mc, request)
+
+ self.assertNotEqual('', response.error)
+
+ def testCheckComponentName_NotAllowedToViewProject(self):
+ self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ request = projects_pb2.CheckComponentNameRequest(
+ project_name='proj',
+ parent_path='Component',
+ component_name='Path')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user_444@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.projects_svcr.CheckComponentName, mc, request)
+
+ def testCheckComponentName_ParentComponentDoesntExist(self):
+ request = projects_pb2.CheckComponentNameRequest(
+ project_name='proj',
+ parent_path='Component',
+ component_name='Path')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(exceptions.NoSuchComponentException):
+ self.CallWrapped(self.projects_svcr.CheckComponentName, mc, request)
+
+ def testCheckFieldName_OK(self):
+ request = projects_pb2.CheckFieldNameRequest(
+ project_name='proj',
+ field_name='Foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+ self.assertEqual('', response.error)
+
+ def testCheckFieldName_InvalidFieldName(self):
+ request = projects_pb2.CheckFieldNameRequest(
+ project_name='proj',
+ field_name='**Foo**')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+ self.assertNotEqual('', response.error)
+
+ def testCheckFieldName_InvalidFieldName_ApproverSuffix(self):
+ request = projects_pb2.CheckFieldNameRequest(
+ project_name='proj',
+ field_name='Foo-aPprOver')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+ self.assertNotEqual('', response.error)
+
+ def testCheckFieldName_FieldAlreadyExists(self):
+ self.AddField('Foo')
+ request = projects_pb2.CheckFieldNameRequest(
+ project_name='proj',
+ field_name='Foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+ self.assertNotEqual('', response.error)
+
+ def testCheckFieldName_FieldIsPrefixOfAnother(self):
+ self.AddField('Foo-Bar')
+ request = projects_pb2.CheckFieldNameRequest(
+ project_name='proj',
+ field_name='Foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+ self.assertNotEqual('', response.error)
+
+ def testCheckFieldName_AnotherFieldIsPrefix(self):
+ self.AddField('Foo')
+ request = projects_pb2.CheckFieldNameRequest(
+ project_name='proj',
+ field_name='Foo-Bar')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+ self.assertNotEqual('', response.error)
+
+ def testCheckFieldName_NotAllowedToViewProject(self):
+ self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ request = projects_pb2.CheckFieldNameRequest(
+ project_name='proj',
+ field_name='Foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user_444@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
diff --git a/api/test/resource_name_converters_test.py b/api/test/resource_name_converters_test.py
new file mode 100644
index 0000000..e9ca437
--- /dev/null
+++ b/api/test/resource_name_converters_test.py
@@ -0,0 +1,773 @@
+# Copyright 2018 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 converting between resource names and external ids."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from mock import Mock, patch
+import unittest
+import re
+
+from api import resource_name_converters as rnc
+from framework import exceptions
+from testing import fake
+from services import service_manager
+from proto import tracker_pb2
+
+class ResourceNameConverterTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ issue=fake.IssueService(),
+ project=fake.ProjectService(),
+ user=fake.UserService(),
+ features=fake.FeaturesService(),
+ template=fake.TemplateService(),
+ config=fake.ConfigService())
+ self.cnxn = fake.MonorailConnection()
+ self.PAST_TIME = 12345
+ self.project_1 = self.services.project.TestAddProject(
+ 'proj', project_id=789)
+ self.project_2 = self.services.project.TestAddProject(
+ 'goose', project_id=788)
+ self.dne_project_id = 1999
+
+ self.issue_1 = fake.MakeTestIssue(
+ self.project_1.project_id, 1, 'sum', 'New', 111,
+ project_name=self.project_1.project_name)
+ self.issue_2 = fake.MakeTestIssue(
+ self.project_2.project_id, 2, 'sum', 'New', 111,
+ project_name=self.project_2.project_name)
+ self.services.issue.TestAddIssue(self.issue_1)
+ self.services.issue.TestAddIssue(self.issue_2)
+
+ self.user_1 = self.services.user.TestAddUser('user_111@example.com', 111)
+ self.user_2 = self.services.user.TestAddUser('user_222@example.com', 222)
+ self.user_3 = self.services.user.TestAddUser('user_333@example.com', 333)
+
+ hotlist_items = [
+ (self.issue_1.issue_id, 9, self.user_2.user_id, self.PAST_TIME, 'note'),
+ (self.issue_2.issue_id, 1, self.user_1.user_id, self.PAST_TIME, 'note')]
+ self.hotlist_1 = self.services.features.TestAddHotlist(
+ 'HotlistName', owner_ids=[], editor_ids=[],
+ hotlist_item_fields=hotlist_items)
+
+ self.template_1 = self.services.template.TestAddIssueTemplateDef(
+ 1, self.project_1.project_id, 'template_1_name')
+ self.template_2 = self.services.template.TestAddIssueTemplateDef(
+ 2, self.project_2.project_id, 'template_2_name')
+ self.dne_template_id = 3
+
+ self.field_def_1_name = 'test_field'
+ self.field_def_1 = self.services.config.CreateFieldDef(
+ self.cnxn, self.project_1.project_id, self.field_def_1_name, 'STR_TYPE',
+ None, None, None, None, None, None, None, None, None, None, None, None,
+ None, None, [], [])
+ self.approval_def_1_name = 'approval_field_1'
+ self.approval_def_1_id = self.services.config.CreateFieldDef(
+ self.cnxn, self.project_1.project_id, self.approval_def_1_name,
+ 'APPROVAL_TYPE', None, None, None, None, None, None, None, None, None,
+ None, None, None, None, None, [], [])
+ self.component_def_1_path = 'Foo'
+ self.component_def_1_id = self.services.config.CreateComponentDef(
+ self.cnxn, self.project_1.project_id, self.component_def_1_path, '',
+ False, [], [], None, 111, [])
+ self.component_def_2_path = 'Foo>Bar>Hey123_I-am-valid'
+ self.component_def_2_id = self.services.config.CreateComponentDef(
+ self.cnxn, self.project_1.project_id, self.component_def_2_path, '',
+ False, [], [], None, 111, [])
+ self.component_def_3_path = 'Fizz'
+ self.component_def_3_id = self.services.config.CreateComponentDef(
+ self.cnxn, self.project_2.project_id, self.component_def_3_path, '',
+ False, [], [], None, 111, [])
+ self.dne_component_def_id = 999
+ self.dne_field_def_id = 999999
+ self.psq_1 = tracker_pb2.SavedQuery(
+ query_id=2, name='psq1 name', base_query_id=1, query='foo=bar')
+ self.psq_2 = tracker_pb2.SavedQuery(
+ query_id=3, name='psq2 name', base_query_id=1, query='fizz=buzz')
+ self.dne_psq_id = 987
+ self.services.features.UpdateCannedQueries(
+ self.cnxn, self.project_1.project_id, [self.psq_1, self.psq_2])
+
+ def testGetResourceNameMatch(self):
+ """We can get a resource name match."""
+ regex = re.compile(r'name\/(?P<group_name>[a-z]+)$')
+ match = rnc._GetResourceNameMatch('name/honque', regex)
+ self.assertEqual(match.group('group_name'), 'honque')
+
+ def testGetResouceNameMatch_InvalidName(self):
+ """An exception is raised if there is not match."""
+ regex = re.compile(r'name\/(?P<group_name>[a-z]+)$')
+ with self.assertRaises(exceptions.InputException):
+ rnc._GetResourceNameMatch('honque/honque', regex)
+
+ def testIngestApprovalDefName(self):
+ approval_id = rnc.IngestApprovalDefName(
+ self.cnxn, 'projects/proj/approvalDefs/123', self.services)
+ self.assertEqual(approval_id, 123)
+
+ def testIngestApprovalDefName_InvalidName(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestApprovalDefName(
+ self.cnxn, 'projects/proj/approvalDefs/123d', self.services)
+
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestApprovalDefName(
+ self.cnxn, 'projects/garbage/approvalDefs/123', self.services)
+
+ def testIngestFieldDefName(self):
+ """We can get a FieldDef's resource name match."""
+ self.assertEqual(
+ rnc.IngestFieldDefName(
+ self.cnxn, 'projects/proj/fieldDefs/123', self.services),
+ (789, 123))
+
+ def testIngestFieldDefName_InvalidName(self):
+ """An exception is raised if the FieldDef's resource name is invalid"""
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestFieldDefName(
+ self.cnxn, 'projects/proj/fieldDefs/7Dog', self.services)
+
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestFieldDefName(
+ self.cnxn, 'garbage/proj/fieldDefs/123', self.services)
+
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestFieldDefName(
+ self.cnxn, 'projects/garbage/fieldDefs/123', self.services)
+
+ def testIngestHotlistName(self):
+ """We can get a Hotlist's resource name match."""
+ self.assertEqual(rnc.IngestHotlistName('hotlists/78909'), 78909)
+
+ def testIngestHotlistName_InvalidName(self):
+ """An exception is raised if the Hotlist's resource name is invalid"""
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestHotlistName('hotlists/789honk789')
+
+ def testIngestHotlistItemNames(self):
+ """We can get Issue IDs from HotlistItems resource names."""
+ names = [
+ 'hotlists/78909/items/proj.1',
+ 'hotlists/78909/items/goose.2']
+ self.assertEqual(
+ rnc.IngestHotlistItemNames(self.cnxn, names, self.services),
+ [self.issue_1.issue_id, self.issue_2.issue_id])
+
+ def testIngestHotlistItemNames_ProjectNotFound(self):
+ """Exception is raised if a project is not found."""
+ names = [
+ 'hotlists/78909/items/proj.1',
+ 'hotlists/78909/items/chicken.2']
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestHotlistItemNames(self.cnxn, names, self.services)
+
+ def testIngestHotlistItemNames_MultipleProjectsNotFound(self):
+ """Aggregated exceptions raised if projects are not found."""
+ names = [
+ 'hotlists/78909/items/proj.1', 'hotlists/78909/items/chicken.2',
+ 'hotlists/78909/items/cow.3'
+ ]
+ with self.assertRaisesRegexp(exceptions.NoSuchProjectException,
+ 'Project chicken not found.\n' +
+ 'Project cow not found.'):
+ rnc.IngestHotlistItemNames(self.cnxn, names, self.services)
+
+ def testIngestHotlistItems_IssueNotFound(self):
+ """Exception is raised if an Issue is not found."""
+ names = [
+ 'hotlists/78909/items/proj.1',
+ 'hotlists/78909/items/goose.5']
+ with self.assertRaisesRegexp(exceptions.NoSuchIssueException, '%r' % names):
+ rnc.IngestHotlistItemNames(self.cnxn, names, self.services)
+
+ def testConvertHotlistName(self):
+ """We can get a Hotlist's resource name."""
+ self.assertEqual(rnc.ConvertHotlistName(10), 'hotlists/10')
+
+ def testConvertHotlistItemNames(self):
+ """We can get Hotlist items' resource names."""
+ expected_dict = {
+ self.hotlist_1.items[0].issue_id: 'hotlists/7739/items/proj.1',
+ self.hotlist_1.items[1].issue_id: 'hotlists/7739/items/goose.2',
+ }
+ self.assertEqual(
+ rnc.ConvertHotlistItemNames(
+ self.cnxn, self.hotlist_1.hotlist_id, expected_dict.keys(),
+ self.services), expected_dict)
+
+ def testIngestApprovalValueName(self):
+ project_id, issue_id, approval_def_id = rnc.IngestApprovalValueName(
+ self.cnxn, 'projects/proj/issues/1/approvalValues/404', self.services)
+ self.assertEqual(project_id, self.project_1.project_id)
+ self.assertEqual(issue_id, self.issue_1.issue_id)
+ self.assertEqual(404, approval_def_id) # We don't verify it exists.
+
+ def testIngestApprovalValueName_ProjectDoesNotExist(self):
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestApprovalValueName(
+ self.cnxn, 'projects/noproj/issues/1/approvalValues/1', self.services)
+
+ def testIngestApprovalValueName_IssueDoesNotExist(self):
+ with self.assertRaisesRegexp(exceptions.NoSuchIssueException,
+ 'projects/proj/issues/404'):
+ rnc.IngestApprovalValueName(
+ self.cnxn, 'projects/proj/issues/404/approvalValues/1', self.services)
+
+ def testIngestApprovalValueName_InvalidStart(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestApprovalValueName(
+ self.cnxn, 'zprojects/proj/issues/1/approvalValues/1', self.services)
+
+ def testIngestApprovalValueName_InvalidEnd(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestApprovalValueName(
+ self.cnxn, 'projects/proj/issues/1/approvalValues/1z', self.services)
+
+ def testIngestApprovalValueName_InvalidCollection(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestApprovalValueName(
+ self.cnxn, 'projects/proj/issues/1/approvalValue/1', self.services)
+
+ def testIngestIssueName(self):
+ """We can get an Issue global id from its resource name."""
+ self.assertEqual(
+ rnc.IngestIssueName(self.cnxn, 'projects/proj/issues/1', self.services),
+ self.issue_1.issue_id)
+
+ def testIngestIssueName_ProjectDoesNotExist(self):
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestIssueName(self.cnxn, 'projects/noproj/issues/1', self.services)
+
+ def testIngestIssueName_IssueDoesNotExist(self):
+ with self.assertRaises(exceptions.NoSuchIssueException):
+ rnc.IngestIssueName(self.cnxn, 'projects/proj/issues/2', self.services)
+
+ def testIngestIssueName_InvalidLocalId(self):
+ """Issue resource name Local IDs are digits."""
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestIssueName(self.cnxn, 'projects/proj/issues/x', self.services)
+
+ def testIngestIssueName_InvalidProjectId(self):
+ """Project names are more than 1 character."""
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestIssueName(self.cnxn, 'projects/p/issues/1', self.services)
+
+ def testIngestIssueName_InvalidFormat(self):
+ """Issue resource names must begin with the project resource name."""
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestIssueName(self.cnxn, 'issues/1', self.services)
+
+ def testIngestIssueName_Moved(self):
+ """We can get a moved issue."""
+ moved_to_project_id = 987
+ self.services.project.TestAddProject(
+ 'other', project_id=moved_to_project_id)
+ new_issue_id = 1010
+ issue = fake.MakeTestIssue(
+ moved_to_project_id, 200, 'sum', 'New', 111, issue_id=new_issue_id)
+ self.services.issue.TestAddIssue(issue)
+ self.services.issue.TestAddMovedIssueRef(
+ self.project_1.project_id, 404, moved_to_project_id, 200)
+
+ self.assertEqual(
+ rnc.IngestIssueName(
+ self.cnxn, 'projects/proj/issues/404', self.services), new_issue_id)
+
+ def testIngestIssueNames(self):
+ """We can get an Issue global ids from resource names."""
+ self.assertEqual(
+ rnc.IngestIssueNames(
+ self.cnxn, ['projects/proj/issues/1', 'projects/goose/issues/2'],
+ self.services), [self.issue_1.issue_id, self.issue_2.issue_id])
+
+ def testIngestIssueNames_EmptyList(self):
+ """We get an empty list when providing an empty list of issue names."""
+ self.assertEqual(rnc.IngestIssueNames(self.cnxn, [], self.services), [])
+
+ def testIngestIssueNames_WithBadInputs(self):
+ """We aggregate input exceptions."""
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'Invalid resource name: projects/proj/badformat/1.\n' +
+ 'Invalid resource name: badformat/proj/issues/1.'):
+ rnc.IngestIssueNames(
+ self.cnxn, [
+ 'projects/proj/badformat/1', 'badformat/proj/issues/1',
+ 'projects/proj/issues/1'
+ ], self.services)
+
+ def testIngestIssueNames_OneDoesNotExist(self):
+ """We get an exception if one issue name provided does not exist."""
+ with self.assertRaises(exceptions.NoSuchIssueException):
+ rnc.IngestIssueNames(
+ self.cnxn, ['projects/proj/issues/1', 'projects/proj/issues/2'],
+ self.services)
+
+ def testIngestIssueNames_ManyDoNotExist(self):
+ """We get an exception if one issue name provided does not exist."""
+ dne_issues = ['projects/proj/issues/2', 'projects/proj/issues/3']
+ with self.assertRaisesRegexp(exceptions.NoSuchIssueException,
+ '%r' % dne_issues):
+ rnc.IngestIssueNames(self.cnxn, dne_issues, self.services)
+
+ def testIngestIssueNames_ProjectsNotExist(self):
+ """Aggregated exceptions raised if projects are not found."""
+ with self.assertRaisesRegexp(exceptions.NoSuchProjectException,
+ 'Project chicken not found.\n' +
+ 'Project cow not found.'):
+ rnc.IngestIssueNames(
+ self.cnxn, [
+ 'projects/chicken/issues/2', 'projects/cow/issues/3',
+ 'projects/proj/issues/1'
+ ], self.services)
+
+ def testIngestProjectFromIssue(self):
+ self.assertEqual(rnc.IngestProjectFromIssue('projects/xyz/issues/1'), 'xyz')
+
+ def testIngestProjectFromIssue_InvalidName(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestProjectFromIssue('projects/xyz')
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestProjectFromIssue('garbage')
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestProjectFromIssue('projects/xyz/issues/garbage')
+
+ def testIngestCommentName(self):
+ name = 'projects/proj/issues/1/comments/0'
+ actual = rnc.IngestCommentName(self.cnxn, name, self.services)
+ self.assertEqual(
+ actual, (self.project_1.project_id, self.issue_1.issue_id, 0))
+
+ def testIngestCommentName_InputException(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestCommentName(self.cnxn, 'misspelled name', self.services)
+
+ def testIngestCommentName_NoSuchProject(self):
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestCommentName(
+ self.cnxn, 'projects/doesnotexist/issues/1/comments/0', self.services)
+
+ def testIngestCommentName_NoSuchIssue(self):
+ with self.assertRaisesRegexp(exceptions.NoSuchIssueException,
+ "['projects/proj/issues/404']"):
+ rnc.IngestCommentName(
+ self.cnxn, 'projects/proj/issues/404/comments/0', self.services)
+
+ def testConvertCommentNames(self):
+ """We can create comment names."""
+ expected = {
+ 0: 'projects/proj/issues/1/comments/0',
+ 1: 'projects/proj/issues/1/comments/1'
+ }
+ self.assertEqual(rnc.CreateCommentNames(1, 'proj', [0, 1]), expected)
+
+ def testConvertCommentNames_Empty(self):
+ """Converting an empty list of comments returns an empty dict."""
+ self.assertEqual(rnc.CreateCommentNames(1, 'proj', []), {})
+
+ def testConvertIssueName(self):
+ """We can create an Issue resource name from an issue_id."""
+ self.assertEqual(
+ rnc.ConvertIssueName(self.cnxn, self.issue_1.issue_id, self.services),
+ 'projects/proj/issues/1')
+
+ def testConvertIssueName_NotFound(self):
+ """Exception is raised if the issue is not found."""
+ with self.assertRaises(exceptions.NoSuchIssueException):
+ rnc.ConvertIssueName(self.cnxn, 3279, self.services)
+
+ def testConvertIssueNames(self):
+ """We can create Issue resource names from issue_ids."""
+ self.assertEqual(
+ rnc.ConvertIssueNames(
+ self.cnxn, [self.issue_1.issue_id, 3279], self.services),
+ {self.issue_1.issue_id: 'projects/proj/issues/1'})
+
+ def testConvertApprovalValueNames(self):
+ """We can create ApprovalValue resource names."""
+ self.issue_1.approval_values = [tracker_pb2.ApprovalValue(
+ approval_id=self.approval_def_1_id)]
+
+ expected_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % self.approval_def_1_id)
+ self.assertEqual(
+ {self.approval_def_1_id: expected_name},
+ rnc.ConvertApprovalValueNames(
+ self.cnxn, self.issue_1.issue_id, self.services))
+
+ def testIngestUserName(self):
+ """We can get a User ID from User resource name."""
+ name = 'users/111'
+ self.assertEqual(rnc.IngestUserName(self.cnxn, name, self.services), 111)
+
+ def testIngestUserName_DisplayName(self):
+ """We can get a User ID from User resource name with a display name set."""
+ name = 'users/%s' % self.user_3.email
+ self.assertEqual(rnc.IngestUserName(self.cnxn, name, self.services), 333)
+
+ def testIngestUserName_NoSuchUser(self):
+ """When autocreate=False, we raise an exception if a user is not found."""
+ name = 'users/chicken@test.com'
+ with self.assertRaises(exceptions.NoSuchUserException):
+ rnc.IngestUserName(self.cnxn, name, self.services)
+
+ def testIngestUserName_InvalidEmail(self):
+ """We raise an exception if a given resource name's email is invalid."""
+ name = 'users/chickentest.com'
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestUserName(self.cnxn, name, self.services)
+
+ def testIngestUserName_Autocreate(self):
+ """When autocreate=True create a new User if they don't already exist."""
+ new_email = 'chicken@test.com'
+ name = 'users/%s' % new_email
+ user_id = rnc.IngestUserName(
+ self.cnxn, name, self.services, autocreate=True)
+
+ new_id = self.services.user.LookupUserID(
+ self.cnxn, new_email, autocreate=False)
+ self.assertEqual(new_id, user_id)
+
+ def testIngestUserNames(self):
+ """We can get User IDs from User resource names."""
+ names = ['users/111', 'users/222', 'users/%s' % self.user_3.email]
+ expected_ids = [111, 222, 333]
+ self.assertEqual(
+ rnc.IngestUserNames(self.cnxn, names, self.services), expected_ids)
+
+ def testIngestUserNames_NoSuchUser(self):
+ """When autocreate=False, we raise an exception if a user is not found."""
+ names = [
+ 'users/111', 'users/chicken@test.com',
+ 'users/%s' % self.user_3.email
+ ]
+ with self.assertRaises(exceptions.NoSuchUserException):
+ rnc.IngestUserNames(self.cnxn, names, self.services)
+
+ def testIngestUserNames_InvalidEmail(self):
+ """We raise an exception if a given resource name's email is invalid."""
+ names = [
+ 'users/111', 'users/chickentest.com',
+ 'users/%s' % self.user_3.email
+ ]
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestUserNames(self.cnxn, names, self.services)
+
+ def testIngestUserNames_Autocreate(self):
+ """When autocreate=True we create new Users if they don't already exist."""
+ new_email = 'user_444@example.com'
+ names = [
+ 'users/111',
+ 'users/%s' % new_email,
+ 'users/%s' % self.user_3.email
+ ]
+ ids = rnc.IngestUserNames(self.cnxn, names, self.services, autocreate=True)
+
+ new_id = self.services.user.LookupUserID(
+ self.cnxn, new_email, autocreate=False)
+ expected_ids = [111, new_id, 333]
+ self.assertEqual(expected_ids, ids)
+
+ def testConvertUserName(self):
+ """We can convert a single User ID to resource name."""
+ self.assertEqual(rnc.ConvertUserName(111), 'users/111')
+
+ def testConvertUserNames(self):
+ """We can get User resource names."""
+ expected_dict = {111: 'users/111', 222: 'users/222', 333: 'users/333'}
+ self.assertEqual(rnc.ConvertUserNames(expected_dict.keys()), expected_dict)
+
+ def testConvertUserNames_Empty(self):
+ """We can process an empty Users list."""
+ self.assertEqual(rnc.ConvertUserNames([]), {})
+
+ def testConvertProjectStarName(self):
+ """We can convert a User ID and Project ID to resource name."""
+ name = rnc.ConvertProjectStarName(
+ self.cnxn, 111, self.project_1.project_id, self.services)
+ expected = 'users/111/projectStars/{}'.format(self.project_1.project_name)
+ self.assertEqual(name, expected)
+
+ def testConvertProjectStarName_NoSuchProjectException(self):
+ """Throws an exception when Project ID is invalid."""
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertProjectStarName(self.cnxn, 111, 123455, self.services)
+
+ def testIngestProjectName(self):
+ """We can get project name from Project resource names."""
+ name = 'projects/{}'.format(self.project_1.project_name)
+ expected = self.project_1.project_id
+ self.assertEqual(
+ rnc.IngestProjectName(self.cnxn, name, self.services), expected)
+
+ def testIngestProjectName_InvalidName(self):
+ """An exception is raised if the Hotlist's resource name is invalid"""
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestProjectName(self.cnxn, 'projects/', self.services)
+
+ def testConvertTemplateNames(self):
+ """We can get IssueTemplate resource names."""
+ expected_resource_name = 'projects/{}/templates/{}'.format(
+ self.project_1.project_name, self.template_1.template_id)
+ expected = {self.template_1.template_id: expected_resource_name}
+
+ self.assertEqual(
+ rnc.ConvertTemplateNames(
+ self.cnxn, self.project_1.project_id, [self.template_1.template_id],
+ self.services), expected)
+
+ def testConvertTemplateNames_NoSuchProjectException(self):
+ """We get an exception if project with id does not exist."""
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertTemplateNames(
+ self.cnxn, self.dne_project_id, [self.template_1.template_id],
+ self.services)
+
+ def testConvertTemplateNames_NonExistentTemplate(self):
+ """We only return templates that exist."""
+ self.assertEqual(
+ rnc.ConvertTemplateNames(
+ self.cnxn, self.project_1.project_id, [self.dne_template_id],
+ self.services), {})
+
+ def testConvertTemplateNames_TemplateInProject(self):
+ """We only return templates in the project."""
+ expected_resource_name = 'projects/{}/templates/{}'.format(
+ self.project_2.project_name, self.template_2.template_id)
+ expected = {self.template_2.template_id: expected_resource_name}
+
+ self.assertEqual(
+ rnc.ConvertTemplateNames(
+ self.cnxn, self.project_2.project_id,
+ [self.template_1.template_id, self.template_2.template_id],
+ self.services), expected)
+
+ def testIngestTemplateName(self):
+ name = 'projects/{}/templates/{}'.format(
+ self.project_1.project_name, self.template_1.template_id)
+ actual = rnc.IngestTemplateName(self.cnxn, name, self.services)
+ expected = (self.template_1.template_id, self.project_1.project_id)
+ self.assertEqual(actual, expected)
+
+ def testIngestTemplateName_DoesNotExist(self):
+ """We will ingest templates that don't exist."""
+ name = 'projects/{}/templates/{}'.format(
+ self.project_1.project_name, self.dne_template_id)
+ actual = rnc.IngestTemplateName(self.cnxn, name, self.services)
+ expected = (self.dne_template_id, self.project_1.project_id)
+ self.assertEqual(actual, expected)
+
+ def testIngestTemplateName_InvalidName(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestTemplateName(
+ self.cnxn, 'projects/asdf/misspelled_template/123', self.services)
+
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestTemplateName(
+ self.cnxn, 'projects/asdf/templates/123', self.services)
+
+ def testConvertStatusDefNames(self):
+ """We can get Status resource name."""
+ expected_resource_name = 'projects/{}/statusDefs/{}'.format(
+ self.project_1.project_name, self.issue_1.status)
+
+ actual = rnc.ConvertStatusDefNames(
+ self.cnxn, [self.issue_1.status], self.project_1.project_id,
+ self.services)
+ self.assertEqual(actual[self.issue_1.status], expected_resource_name)
+
+ def testConvertStatusDefNames_NoSuchProjectException(self):
+ """We can get an exception if project with id does not exist."""
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertStatusDefNames(
+ self.cnxn, [self.issue_1.status], self.dne_project_id, self.services)
+
+ def testConvertLabelDefNames(self):
+ """We can get Label resource names."""
+ expected_label = 'some label'
+ expected_resource_name = 'projects/{}/labelDefs/{}'.format(
+ self.project_1.project_name, expected_label)
+
+ self.assertEqual(
+ rnc.ConvertLabelDefNames(
+ self.cnxn, [expected_label], self.project_1.project_id,
+ self.services), {expected_label: expected_resource_name})
+
+ def testConvertLabelDefNames_NoSuchProjectException(self):
+ """We can get an exception if project with id does not exist."""
+ some_label = 'some label'
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertLabelDefNames(
+ self.cnxn, [some_label], self.dne_project_id, self.services)
+
+ def testConvertComponentDefNames(self):
+ """We can get Component resource names."""
+ expected_id = 123456
+ expected_resource_name = 'projects/{}/componentDefs/{}'.format(
+ self.project_1.project_name, expected_id)
+
+ self.assertEqual(
+ rnc.ConvertComponentDefNames(
+ self.cnxn, [expected_id], self.project_1.project_id, self.services),
+ {expected_id: expected_resource_name})
+
+ def testConvertComponentDefNames_NoSuchProjectException(self):
+ """We can get an exception if project with id does not exist."""
+ component_id = 123456
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertComponentDefNames(
+ self.cnxn, [component_id], self.dne_project_id, self.services)
+
+ def testIngestComponentDefNames(self):
+ names = [
+ 'projects/proj/componentDefs/%d' % self.component_def_1_id,
+ 'projects/proj/componentDefs/%s' % self.component_def_2_path
+ ]
+ actual = rnc.IngestComponentDefNames(self.cnxn, names, self.services)
+ self.assertEqual(actual, [
+ (self.project_1.project_id, self.component_def_1_id),
+ (self.project_1.project_id, self.component_def_2_id)])
+
+ def testIngestComponentDefNames_NoSuchProjectException(self):
+ names = ['projects/xyz/componentDefs/%d' % self.component_def_1_id]
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestComponentDefNames(self.cnxn, names, self.services)
+
+ names = ['projects/xyz/componentDefs/1', 'projects/zyz/componentDefs/1']
+ expected_error = 'Project not found: xyz.\nProject not found: zyz.'
+ with self.assertRaisesRegexp(exceptions.NoSuchProjectException,
+ expected_error):
+ rnc.IngestComponentDefNames(self.cnxn, names, self.services)
+
+ def testIngestComponentDefNames_NoSuchComponentException(self):
+ names = ['projects/proj/componentDefs/%d' % self.dne_component_def_id]
+ with self.assertRaises(exceptions.NoSuchComponentException):
+ rnc.IngestComponentDefNames(self.cnxn, names, self.services)
+
+ names = [
+ 'projects/proj/componentDefs/999', 'projects/proj/componentDefs/cow'
+ ]
+ expected_error = 'Component not found: 999.\nComponent not found: \'cow\'.'
+ with self.assertRaisesRegexp(exceptions.NoSuchComponentException,
+ expected_error):
+ rnc.IngestComponentDefNames(self.cnxn, names, self.services)
+
+ def testIngestComponentDefNames_InvalidNames(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestComponentDefNames(
+ self.cnxn, ['projects/proj/componentDefs/not.path.or.id'],
+ self.services)
+
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestComponentDefNames(
+ self.cnxn, ['projects/proj/componentDefs/Foo>'], self.services)
+
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestComponentDefNames(
+ self.cnxn, ['projects/proj/componentDefs/>Bar'], self.services)
+
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestComponentDefNames(
+ self.cnxn, ['projects/proj/componentDefs/Foo>123Bar'], self.services)
+
+ def testIngestComponentDefNames_CrossProject(self):
+ names = [
+ 'projects/proj/componentDefs/%d' % self.component_def_1_id,
+ 'projects/goose/componentDefs/%s' % self.component_def_3_path
+ ]
+ actual = rnc.IngestComponentDefNames(self.cnxn, names, self.services)
+ self.assertEqual(actual, [
+ (self.project_1.project_id, self.component_def_1_id),
+ (self.project_2.project_id, self.component_def_3_id)])
+
+ def testConvertFieldDefNames(self):
+ """Returns resource names for fields that exist and ignores others."""
+ expected_key = self.field_def_1
+ expected_value = 'projects/{}/fieldDefs/{}'.format(
+ self.project_1.project_name, self.field_def_1)
+
+ field_ids = [self.field_def_1, self.dne_field_def_id]
+ self.assertEqual(
+ rnc.ConvertFieldDefNames(
+ self.cnxn, field_ids, self.project_1.project_id, self.services),
+ {expected_key: expected_value})
+
+ def testConvertFieldDefNames_NoSuchProjectException(self):
+ field_ids = [self.field_def_1, self.dne_field_def_id]
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertFieldDefNames(
+ self.cnxn, field_ids, self.dne_project_id, self.services)
+
+ def testConvertApprovalDefNames(self):
+ outcome = rnc.ConvertApprovalDefNames(
+ self.cnxn, [self.approval_def_1_id], self.project_1.project_id,
+ self.services)
+
+ expected_key = self.approval_def_1_id
+ expected_value = 'projects/{}/approvalDefs/{}'.format(
+ self.project_1.project_name, self.approval_def_1_id)
+ self.assertEqual(outcome, {expected_key: expected_value})
+
+ def testConvertApprovalDefNames_NoSuchProjectException(self):
+ approval_ids = [self.approval_def_1_id]
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertApprovalDefNames(
+ self.cnxn, approval_ids, self.dne_project_id, self.services)
+
+ def testConvertProjectName(self):
+ self.assertEqual(
+ rnc.ConvertProjectName(
+ self.cnxn, self.project_1.project_id, self.services),
+ 'projects/{}'.format(self.project_1.project_name))
+
+ def testConvertProjectName_NoSuchProjectException(self):
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertProjectName(self.cnxn, self.dne_project_id, self.services)
+
+ def testConvertProjectConfigName(self):
+ self.assertEqual(
+ rnc.ConvertProjectConfigName(
+ self.cnxn, self.project_1.project_id, self.services),
+ 'projects/{}/config'.format(self.project_1.project_name))
+
+ def testConvertProjectConfigName_NoSuchProjectException(self):
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertProjectConfigName(
+ self.cnxn, self.dne_project_id, self.services)
+
+ def testConvertProjectMemberName(self):
+ self.assertEqual(
+ rnc.ConvertProjectMemberName(
+ self.cnxn, self.project_1.project_id, 111, self.services),
+ 'projects/{}/members/{}'.format(self.project_1.project_name, 111))
+
+ def testConvertProjectMemberName_NoSuchProjectException(self):
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertProjectMemberName(
+ self.cnxn, self.dne_project_id, 111, self.services)
+
+ def testConvertProjectSavedQueryNames(self):
+ query_ids = [self.psq_1.query_id, self.psq_2.query_id, self.dne_psq_id]
+ outcome = rnc.ConvertProjectSavedQueryNames(
+ self.cnxn, query_ids, self.project_1.project_id, self.services)
+
+ expected_value_1 = 'projects/{}/savedQueries/{}'.format(
+ self.project_1.project_name, self.psq_1.name)
+ expected_value_2 = 'projects/{}/savedQueries/{}'.format(
+ self.project_1.project_name, self.psq_2.name)
+ self.assertEqual(
+ outcome, {
+ self.psq_1.query_id: expected_value_1,
+ self.psq_2.query_id: expected_value_2
+ })
+
+ def testConvertProjectSavedQueryNames_NoSuchProjectException(self):
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertProjectSavedQueryNames(
+ self.cnxn, [self.psq_1.query_id], self.dne_project_id, self.services)
diff --git a/api/test/sitewide_servicer_test.py b/api/test/sitewide_servicer_test.py
new file mode 100644
index 0000000..3259fbb
--- /dev/null
+++ b/api/test/sitewide_servicer_test.py
@@ -0,0 +1,143 @@
+# Copyright 2018 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 sitewide servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import time
+import unittest
+
+import mock
+from components.prpc import codes
+from components.prpc import context
+from components.prpc import server
+
+import settings
+from api import sitewide_servicer
+from api.api_proto import common_pb2
+from api.api_proto import sitewide_pb2
+from framework import monorailcontext
+from framework import xsrf
+from services import service_manager
+from testing import fake
+
+
+class SitewideServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ usergroup=fake.UserGroupService(),
+ user=fake.UserService())
+ self.user_1 = self.services.user.TestAddUser('owner@example.com', 111)
+ self.sitewide_svcr = sitewide_servicer.SitewideServicer(
+ self.services, make_rate_limiter=False)
+
+ def CallWrapped(self, wrapped_handler, *args, **kwargs):
+ return wrapped_handler.wrapped(self.sitewide_svcr, *args, **kwargs)
+
+ @mock.patch('services.secrets_svc.GetXSRFKey')
+ @mock.patch('time.time')
+ def testRefreshToken(self, mockTime, mockGetXSRFKey):
+ """We can refresh an expired token."""
+ mockGetXSRFKey.side_effect = lambda: 'fakeXSRFKey'
+ # The token is at the brink of being too old
+ mockTime.side_effect = lambda: 1 + xsrf.REFRESH_TOKEN_TIMEOUT_SEC
+
+ token_path = 'token_path'
+ token = xsrf.GenerateToken(111, token_path, 1)
+
+ request = sitewide_pb2.RefreshTokenRequest(
+ token=token, token_path=token_path)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.sitewide_svcr.RefreshToken, mc, request)
+
+ self.assertEqual(
+ sitewide_pb2.RefreshTokenResponse(
+ token='QSaKMyXhY752g7n8a34HyTo4NjQwMDE=',
+ token_expires_sec=870901),
+ response)
+
+ @mock.patch('services.secrets_svc.GetXSRFKey')
+ @mock.patch('time.time')
+ def testRefreshToken_InvalidToken(self, mockTime, mockGetXSRFKey):
+ """We reject attempts to refresh an invalid token."""
+ mockGetXSRFKey.side_effect = ['fakeXSRFKey']
+ mockTime.side_effect = [123]
+
+ token_path = 'token_path'
+ token = 'invalidToken'
+
+ request = sitewide_pb2.RefreshTokenRequest(
+ token=token, token_path=token_path)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ with self.assertRaises(xsrf.TokenIncorrect):
+ self.CallWrapped(self.sitewide_svcr.RefreshToken, mc, request)
+
+ @mock.patch('services.secrets_svc.GetXSRFKey')
+ @mock.patch('time.time')
+ def testRefreshToken_TokenTooOld(self, mockTime, mockGetXSRFKey):
+ """We reject attempts to refresh a token that's too old."""
+ mockGetXSRFKey.side_effect = lambda: 'fakeXSRFKey'
+ mockTime.side_effect = lambda: 2 + xsrf.REFRESH_TOKEN_TIMEOUT_SEC
+
+ token_path = 'token_path'
+ token = xsrf.GenerateToken(111, token_path, 1)
+
+ request = sitewide_pb2.RefreshTokenRequest(
+ token=token, token_path=token_path)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ with self.assertRaises(xsrf.TokenIncorrect):
+ self.CallWrapped(self.sitewide_svcr.RefreshToken, mc, request)
+
+ def testGetServerStatus_Normal(self):
+ request = sitewide_pb2.GetServerStatusRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.sitewide_svcr.GetServerStatus, mc, request)
+
+ self.assertEqual(
+ sitewide_pb2.GetServerStatusResponse(),
+ response)
+
+ @mock.patch('settings.banner_message', 'Message')
+ def testGetServerStatus_BannerMessage(self):
+ request = sitewide_pb2.GetServerStatusRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.sitewide_svcr.GetServerStatus, mc, request)
+
+ self.assertEqual(
+ sitewide_pb2.GetServerStatusResponse(banner_message='Message'),
+ response)
+
+ @mock.patch('settings.banner_time', (2019, 6, 13, 18, 30))
+ def testGetServerStatus_BannerTime(self):
+ request = sitewide_pb2.GetServerStatusRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.sitewide_svcr.GetServerStatus, mc, request)
+
+ self.assertEqual(
+ sitewide_pb2.GetServerStatusResponse(banner_time=1560450600),
+ response)
+
+ @mock.patch('settings.read_only', True)
+ def testGetServerStatus_ReadOnly(self):
+ request = sitewide_pb2.GetServerStatusRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.sitewide_svcr.GetServerStatus, mc, request)
+
+ self.assertEqual(
+ sitewide_pb2.GetServerStatusResponse(read_only=True),
+ response)
diff --git a/api/test/users_servicer_test.py b/api/test/users_servicer_test.py
new file mode 100644
index 0000000..aa25d18
--- /dev/null
+++ b/api/test/users_servicer_test.py
@@ -0,0 +1,606 @@
+# Copyright 2018 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 users servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import mox
+from components.prpc import codes
+from components.prpc import context
+from components.prpc import server
+
+from api import users_servicer
+from api.api_proto import common_pb2
+from api.api_proto import users_pb2
+from api.api_proto import user_objects_pb2
+from framework import authdata
+from framework import exceptions
+from framework import monorailcontext
+from framework import permissions
+from proto import project_pb2
+from proto import tracker_pb2
+from proto import user_pb2
+from testing import fake
+from services import service_manager
+
+
+class UsersServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.mox = mox.Mox()
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ user_star=fake.UserStarService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ project_star=fake.ProjectStarService(),
+ features=fake.FeaturesService())
+ self.project = self.services.project.TestAddProject('proj', project_id=987)
+ self.user = self.services.user.TestAddUser('owner@example.com', 111)
+ self.user_2 = self.services.user.TestAddUser('test2@example.com', 222)
+ self.group1_id = self.services.usergroup.CreateGroup(
+ self.cnxn, self.services, 'group1@test.com', 'anyone')
+ self.group2_id = self.services.usergroup.CreateGroup(
+ self.cnxn, self.services, 'group2@test.com', 'anyone')
+ self.services.usergroup.UpdateMembers(
+ self.cnxn, self.group1_id, [111], 'member')
+ self.services.usergroup.UpdateMembers(
+ self.cnxn, self.group2_id, [222, 111], 'owner')
+ self.users_svcr = users_servicer.UsersServicer(
+ self.services, make_rate_limiter=False)
+ self.prpc_context = context.ServicerContext()
+ self.prpc_context.set_code(codes.StatusCode.OK)
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def CallWrapped(self, wrapped_handler, *args, **kwargs):
+ return wrapped_handler.wrapped(self.users_svcr, *args, **kwargs)
+
+ def testGetMemberships(self):
+ request = users_pb2.GetMembershipsRequest(
+ user_ref=common_pb2.UserRef(
+ display_name='owner@example.com', user_id=111))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ response = self.CallWrapped(self.users_svcr.GetMemberships, mc, request)
+ expected_group_refs = [
+ common_pb2.UserRef(
+ display_name='group1@test.com', user_id=self.group1_id),
+ common_pb2.UserRef(
+ display_name='group2@test.com', user_id=self.group2_id)
+ ]
+
+ self.assertItemsEqual(expected_group_refs, response.group_refs)
+
+ def testGetMemberships_NonExistentUser(self):
+ request = users_pb2.GetMembershipsRequest(
+ user_ref=common_pb2.UserRef(
+ display_name='ghost@example.com', user_id=888)
+ )
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='')
+
+ with self.assertRaises(exceptions.NoSuchUserException):
+ self.CallWrapped(self.users_svcr.GetMemberships, mc, request)
+
+ def testGetUser(self):
+ """We can get a user by email address."""
+ user_ref = common_pb2.UserRef(display_name='test2@example.com')
+ request = users_pb2.GetUserRequest(user_ref=user_ref)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.users_svcr.GetUser, mc, request)
+ self.assertEqual(response.display_name, 'test2@example.com')
+ self.assertEqual(response.user_id, 222)
+ self.assertFalse(response.is_site_admin)
+
+ self.user_2.is_site_admin = True
+ response = self.CallWrapped(
+ self.users_svcr.GetUser, mc, request)
+ self.assertTrue(response.is_site_admin)
+
+ def testListReferencedUsers(self):
+ """We can get all valid users by email addresses."""
+ request = users_pb2.ListReferencedUsersRequest(
+ # we ignore emails that are empty or belong to non-existent users.
+ user_refs=[
+ common_pb2.UserRef(display_name='test2@example.com'),
+ common_pb2.UserRef(display_name='ghost@example.com'),
+ common_pb2.UserRef(display_name=''),
+ common_pb2.UserRef()])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.users_svcr.ListReferencedUsers, mc, request)
+ self.assertEqual(len(response.users), 1)
+ self.assertEqual(response.users[0].user_id, 222)
+
+ def testListReferencedUsers_Deprecated(self):
+ """We can get all valid users by email addresses."""
+ request = users_pb2.ListReferencedUsersRequest(
+ # we ignore emails that are empty or belong to non-existent users.
+ emails=[
+ 'test2@example.com',
+ 'ghost@example.com',
+ ''])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.users_svcr.ListReferencedUsers, mc, request)
+ self.assertEqual(len(response.users), 1)
+ self.assertEqual(response.users[0].user_id, 222)
+
+ def CallGetStarCount(self):
+ request = users_pb2.GetUserStarCountRequest(
+ user_ref=common_pb2.UserRef(user_id=222))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.users_svcr.GetUserStarCount, mc, request)
+ return response.star_count
+
+ def CallStar(self, requester='owner@example.com', starred=True):
+ request = users_pb2.StarUserRequest(
+ user_ref=common_pb2.UserRef(user_id=222), starred=starred)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=requester)
+ response = self.CallWrapped(
+ self.users_svcr.StarUser, mc, request)
+ return response.star_count
+
+ def testStarCount_Normal(self):
+ self.assertEqual(0, self.CallGetStarCount())
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ def testStarCount_StarTwiceSameUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ def testStarCount_StarTwiceDifferentUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(2, self.CallStar(requester='test2@example.com'))
+ self.assertEqual(2, self.CallGetStarCount())
+
+ def testStarCount_RemoveStarTwiceSameUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ self.assertEqual(0, self.CallStar(starred=False))
+ self.assertEqual(0, self.CallStar(starred=False))
+ self.assertEqual(0, self.CallGetStarCount())
+
+ def testStarCount_RemoveStarTwiceDifferentUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(2, self.CallStar(requester='test2@example.com'))
+ self.assertEqual(2, self.CallGetStarCount())
+
+ self.assertEqual(1, self.CallStar(starred=False))
+ self.assertEqual(
+ 0, self.CallStar(requester='test2@example.com', starred=False))
+ self.assertEqual(0, self.CallGetStarCount())
+
+ def testSetExpandPermsPreference_KeepOpen(self):
+ request = users_pb2.SetExpandPermsPreferenceRequest(expand_perms=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.users_svcr.SetExpandPermsPreference, mc, request)
+
+ user = self.services.user.GetUser(self.cnxn, self.user.user_id)
+ self.assertTrue(user.keep_people_perms_open)
+
+ def testSetExpandPermsPreference_DontKeepOpen(self):
+ request = users_pb2.SetExpandPermsPreferenceRequest(expand_perms=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.users_svcr.SetExpandPermsPreference, mc, request)
+
+ user = self.services.user.GetUser(self.cnxn, self.user.user_id)
+ self.assertFalse(user.keep_people_perms_open)
+
+ def testGetUserSavedQueries_Anon(self):
+ """Anon has empty saved queries."""
+ request = users_pb2.GetSavedQueriesRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=None)
+ response = self.CallWrapped(self.users_svcr.GetSavedQueries, mc, request)
+
+ self.assertEqual(0, len(response.saved_queries))
+
+ def testGetUserSavedQueries_Mine(self):
+ """See your own queries."""
+ self.services.features.UpdateUserSavedQueries(self.cnxn, 111, [
+ tracker_pb2.SavedQuery(query_id=101, name='test', query='owner:me'),
+ tracker_pb2.SavedQuery(query_id=202, name='hello', query='world',
+ executes_in_project_ids=[987])
+ ])
+ request = users_pb2.GetUserPrefsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.users_svcr.GetSavedQueries, mc, request)
+
+ self.assertEqual(2, len(response.saved_queries))
+
+ self.assertEqual('test', response.saved_queries[0].name)
+ self.assertEqual('owner:me', response.saved_queries[0].query)
+ self.assertEqual('hello', response.saved_queries[1].name)
+ self.assertEqual('world', response.saved_queries[1].query)
+ self.assertEqual(['proj'], response.saved_queries[1].project_names)
+
+
+ def testGetUserSavedQueries_Other_Allowed(self):
+ """See other people's queries if you're an admin."""
+ self.services.features.UpdateUserSavedQueries(self.cnxn, 111, [
+ tracker_pb2.SavedQuery(query_id=101, name='test', query='owner:me'),
+ tracker_pb2.SavedQuery(query_id=202, name='hello', query='world',
+ executes_in_project_ids=[987])
+ ])
+ self.user_2.is_site_admin = True
+
+ request = users_pb2.GetSavedQueriesRequest()
+ request.user_ref.display_name = 'owner@example.com'
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+
+ response = self.CallWrapped(self.users_svcr.GetSavedQueries, mc, request)
+
+ self.assertEqual(2, len(response.saved_queries))
+
+ self.assertEqual('test', response.saved_queries[0].name)
+ self.assertEqual('owner:me', response.saved_queries[0].query)
+ self.assertEqual('hello', response.saved_queries[1].name)
+ self.assertEqual('world', response.saved_queries[1].query)
+ self.assertEqual(['proj'], response.saved_queries[1].project_names)
+
+ def testGetUserSavedQueries_Other_Denied(self):
+ """Can't see other people's queries unless you're an admin."""
+ self.services.features.UpdateUserSavedQueries(self.cnxn, 111, [
+ tracker_pb2.SavedQuery(query_id=101, name='test', query='owner:me'),
+ tracker_pb2.SavedQuery(query_id=202, name='hello', query='world',
+ executes_in_project_ids=[987])
+ ])
+
+ request = users_pb2.GetSavedQueriesRequest()
+ request.user_ref.display_name = 'owner@example.com'
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.users_svcr.GetSavedQueries, mc, request)
+
+ def testGetUserPrefs_Anon(self):
+ """Anon always has empty prefs."""
+ request = users_pb2.GetUserPrefsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=None)
+ response = self.CallWrapped(self.users_svcr.GetUserPrefs, mc, request)
+
+ self.assertEqual(0, len(response.prefs))
+
+ def testGetUserPrefs_Mine_Empty(self):
+ """User who never set any pref gets empty prefs."""
+ request = users_pb2.GetUserPrefsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.users_svcr.GetUserPrefs, mc, request)
+
+ self.assertEqual(0, len(response.prefs))
+
+ def testGetUserPrefs_Mine_Some(self):
+ """User who set a pref gets it back."""
+ self.services.user.SetUserPrefs(
+ self.cnxn, 111,
+ [user_pb2.UserPrefValue(name='code_font', value='true')])
+ request = users_pb2.GetUserPrefsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.users_svcr.GetUserPrefs, mc, request)
+
+ self.assertEqual(1, len(response.prefs))
+ self.assertEqual('code_font', response.prefs[0].name)
+ self.assertEqual('true', response.prefs[0].value)
+
+ def testGetUserPrefs_Other_Allowed(self):
+ """A site admin can read another user's prefs."""
+ self.services.user.SetUserPrefs(
+ self.cnxn, 111,
+ [user_pb2.UserPrefValue(name='code_font', value='true')])
+ self.user_2.is_site_admin = True
+
+ request = users_pb2.GetUserPrefsRequest()
+ request.user_ref.display_name = 'owner@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.users_svcr.GetUserPrefs, mc, request)
+
+ self.assertEqual(1, len(response.prefs))
+ self.assertEqual('code_font', response.prefs[0].name)
+ self.assertEqual('true', response.prefs[0].value)
+
+ def testGetUserPrefs_Other_Denied(self):
+ """A non-admin cannot read another user's prefs."""
+ self.services.user.SetUserPrefs(
+ self.cnxn, 111,
+ [user_pb2.UserPrefValue(name='code_font', value='true')])
+ # user2 is not a site admin.
+
+ request = users_pb2.GetUserPrefsRequest()
+ request.user_ref.display_name = 'owner@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.users_svcr.GetUserPrefs, mc, request)
+
+ def testSetUserPrefs_Anon(self):
+ """Anon cannot set prefs."""
+ request = users_pb2.SetUserPrefsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=None)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.users_svcr.SetUserPrefs, mc, request)
+
+ def testSetUserPrefs_Mine_Empty(self):
+ """Setting zero prefs is a no-op.."""
+ request = users_pb2.SetUserPrefsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.users_svcr.SetUserPrefs, mc, request)
+
+ prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
+ self.assertEqual(0, len(prefs_after.prefs))
+
+ def testSetUserPrefs_Mine_Add(self):
+ """User can set a preference for the first time."""
+ request = users_pb2.SetUserPrefsRequest(
+ prefs=[user_objects_pb2.UserPrefValue(name='code_font', value='true')])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.users_svcr.SetUserPrefs, mc, request)
+
+ prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
+ self.assertEqual(1, len(prefs_after.prefs))
+ self.assertEqual('code_font', prefs_after.prefs[0].name)
+ self.assertEqual('true', prefs_after.prefs[0].value)
+
+ def testSetUserPrefs_Mine_Overwrite(self):
+ """User can change the value of a pref."""
+ self.services.user.SetUserPrefs(
+ self.cnxn, 111,
+ [user_pb2.UserPrefValue(name='code_font', value='true')])
+ request = users_pb2.SetUserPrefsRequest(
+ prefs=[user_objects_pb2.UserPrefValue(name='code_font', value='false')])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.users_svcr.SetUserPrefs, mc, request)
+
+ prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
+ self.assertEqual(1, len(prefs_after.prefs))
+ self.assertEqual('code_font', prefs_after.prefs[0].name)
+ self.assertEqual('false', prefs_after.prefs[0].value)
+
+ def testSetUserPrefs_Other_Allowed(self):
+ """A site admin can update another user's prefs."""
+ self.services.user.SetUserPrefs(
+ self.cnxn, 111,
+ [user_pb2.UserPrefValue(name='code_font', value='true')])
+ self.user_2.is_site_admin = True
+
+ request = users_pb2.SetUserPrefsRequest(
+ prefs=[user_objects_pb2.UserPrefValue(name='code_font', value='false')])
+ request.user_ref.display_name = 'owner@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ self.CallWrapped(self.users_svcr.SetUserPrefs, mc, request)
+
+ prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
+ self.assertEqual(1, len(prefs_after.prefs))
+ self.assertEqual('code_font', prefs_after.prefs[0].name)
+ self.assertEqual('false', prefs_after.prefs[0].value)
+
+ def testSetUserPrefs_Other_Denied(self):
+ """A non-admin cannot set another user's prefs."""
+ self.services.user.SetUserPrefs(
+ self.cnxn, 111,
+ [user_pb2.UserPrefValue(name='code_font', value='true')])
+ # user2 is not a site admin.
+
+ request = users_pb2.SetUserPrefsRequest(
+ prefs=[user_objects_pb2.UserPrefValue(name='code_font', value='false')])
+ request.user_ref.display_name = 'owner@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.users_svcr.SetUserPrefs, mc, request)
+
+ # Regardless of any exception, the preferences remain unchanged.
+ prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
+ self.assertEqual(1, len(prefs_after.prefs))
+ self.assertEqual('code_font', prefs_after.prefs[0].name)
+ self.assertEqual('true', prefs_after.prefs[0].value)
+
+ def testInviteLinkedParent_NotFound(self):
+ """Reject attempt to invite a user that does not exist."""
+ self.services.user.TestAddUser('user@google.com', 333)
+ request = users_pb2.InviteLinkedParentRequest(
+ email='who@chromium.org') # Does not exist.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='who@google.com')
+ with self.assertRaises(exceptions.NoSuchUserException):
+ self.CallWrapped(self.users_svcr.InviteLinkedParent, mc, request)
+
+ def testInviteLinkedParent_Normal(self):
+ """We can invite accounts to link when all criteria are met."""
+ self.services.user.TestAddUser('user@google.com', 333)
+ self.services.user.TestAddUser('user@chromium.org', 444)
+ request = users_pb2.InviteLinkedParentRequest(
+ email='user@google.com')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@chromium.org')
+ self.CallWrapped(self.users_svcr.InviteLinkedParent, mc, request)
+
+ (invite_as_parent, invite_as_child
+ ) = self.services.user.GetPendingLinkedInvites(self.cnxn, 333)
+ self.assertEqual([444], invite_as_parent)
+ self.assertEqual([], invite_as_child)
+ (invite_as_parent, invite_as_child
+ ) = self.services.user.GetPendingLinkedInvites(self.cnxn, 444)
+ self.assertEqual([], invite_as_parent)
+ self.assertEqual([333], invite_as_child)
+
+ def testAcceptLinkedChild_NotFound(self):
+ """Reject attempt to link a user that does not exist."""
+ self.services.user.TestAddUser('user@google.com', 333)
+ request = users_pb2.AcceptLinkedChildRequest(
+ email='who@chromium.org') # Does not exist.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='who@google.com')
+ with self.assertRaises(exceptions.NoSuchUserException):
+ self.CallWrapped(self.users_svcr.AcceptLinkedChild, mc, request)
+
+ def testAcceptLinkedChild_NoInvite(self):
+ """Reject attempt to link accounts when there was no invite."""
+ self.services.user.TestAddUser('user@google.com', 333)
+ self.services.user.TestAddUser('user@chromium.org', 444)
+ request = users_pb2.AcceptLinkedChildRequest(
+ email='user@chromium.org')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@google.com')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.users_svcr.AcceptLinkedChild, mc, request)
+
+ def testAcceptLinkedChild_Normal(self):
+ """We can linke accounts when all criteria are met."""
+ parent = self.services.user.TestAddUser('user@google.com', 333)
+ child = self.services.user.TestAddUser('user@chromium.org', 444)
+ self.services.user.InviteLinkedParent(
+ self.cnxn, parent.user_id, child.user_id)
+ request = users_pb2.AcceptLinkedChildRequest(
+ email='user@chromium.org')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@google.com')
+ self.CallWrapped(self.users_svcr.AcceptLinkedChild, mc, request)
+
+ self.assertEqual(parent.user_id, child.linked_parent_id)
+ self.assertIn(child.user_id, parent.linked_child_ids)
+
+ def testUnlinkAccounts_NotFound(self):
+ """Reject attempt to unlink a user that does not exist or unspecified."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ request = users_pb2.UnlinkAccountsRequest(
+ parent=common_pb2.UserRef(display_name='who@chromium.org'),
+ child=common_pb2.UserRef(display_name='owner@example.com'))
+ with self.assertRaises(exceptions.NoSuchUserException):
+ self.CallWrapped(self.users_svcr.UnlinkAccounts, mc, request)
+
+ request = users_pb2.UnlinkAccountsRequest(
+ parent=common_pb2.UserRef(display_name='owner@example.com'),
+ child=common_pb2.UserRef(display_name='who@google.com'))
+ with self.assertRaises(exceptions.NoSuchUserException):
+ self.CallWrapped(self.users_svcr.UnlinkAccounts, mc, request)
+
+ request = users_pb2.UnlinkAccountsRequest(
+ parent=common_pb2.UserRef(display_name='owner@example.com'))
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.users_svcr.UnlinkAccounts, mc, request)
+
+ request = users_pb2.UnlinkAccountsRequest(
+ child=common_pb2.UserRef(display_name='owner@example.com'))
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.users_svcr.UnlinkAccounts, mc, request)
+
+ def testUnlinkAccounts_Normal(self):
+ """Users can unlink their accounts."""
+ self.services.user.linked_account_rows = [(111, 222)]
+ request = users_pb2.UnlinkAccountsRequest(
+ parent=common_pb2.UserRef(display_name='owner@example.com'),
+ child=common_pb2.UserRef(display_name='test2@example.com'))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ self.CallWrapped(self.users_svcr.UnlinkAccounts, mc, request)
+
+ self.assertEqual([], self.services.user.linked_account_rows)
+
+ def AddUserProjects(self, user_id):
+ project_states = {
+ 'live': project_pb2.ProjectState.LIVE,
+ 'archived': project_pb2.ProjectState.ARCHIVED,
+ 'deletable': project_pb2.ProjectState.DELETABLE}
+
+ for name, state in project_states.items():
+ self.services.project.TestAddProject(
+ 'owner-%s-%s' % (name, user_id), state=state, owner_ids=[user_id])
+ self.services.project.TestAddProject(
+ 'committer-%s-%s' % (name, user_id), state=state,\
+ committer_ids=[user_id])
+ contributor = self.services.project.TestAddProject(
+ 'contributor-%s-%s' % (name, user_id), state=state)
+ contributor.contributor_ids = [user_id]
+
+ members_only = self.services.project.TestAddProject(
+ 'members-only-' + str(user_id), owner_ids=[user_id])
+ members_only.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+
+ def testGetUsersProjects(self):
+ self.user = self.services.user.TestAddUser('test3@example.com', 333)
+ self.services.project_star.SetStar(
+ self.cnxn, self.project.project_id, 222, True)
+ self.project.committer_ids.extend([222])
+
+ self.AddUserProjects(222)
+ self.AddUserProjects(333)
+
+ request = users_pb2.GetUsersProjectsRequest(user_refs=[
+ common_pb2.UserRef(display_name='test2@example.com'),
+ common_pb2.UserRef(display_name='test3@example.com')])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.users_svcr.GetUsersProjects, mc, request)
+
+ self.assertEqual([
+ user_objects_pb2.UserProjects(
+ user_ref=common_pb2.UserRef(display_name='test2@example.com'),
+ owner_of=['members-only-222', 'owner-live-222'],
+ member_of=['committer-live-222', 'proj'],
+ contributor_to=['contributor-live-222'],
+ starred_projects=['proj']),
+ user_objects_pb2.UserProjects(
+ user_ref=common_pb2.UserRef(display_name='test3@example.com'),
+ owner_of=['owner-live-333'],
+ member_of=['committer-live-333'],
+ contributor_to=['contributor-live-333'])],
+ list(response.users_projects))
+
+ def testGetUsersProjects_NoUserRefs(self):
+ request = users_pb2.GetUsersProjectsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.users_svcr.GetUsersProjects, mc, request)
+ self.assertEqual([], list(response.users_projects))