Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/tracker/test/__init__.py b/tracker/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tracker/test/__init__.py
diff --git a/tracker/test/attachment_helpers_test.py b/tracker/test/attachment_helpers_test.py
new file mode 100644
index 0000000..18e0efc
--- /dev/null
+++ b/tracker/test/attachment_helpers_test.py
@@ -0,0 +1,149 @@
+# Copyright 2016 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
+
+"""Unittest for the tracker helpers module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from mock import Mock, patch
+import unittest
+
+from proto import tracker_pb2
+from tracker import attachment_helpers
+
+
+class AttachmentHelpersFunctionsTest(unittest.TestCase):
+
+ def testIsViewableImage(self):
+ self.assertTrue(attachment_helpers.IsViewableImage('image/gif', 123))
+ self.assertTrue(attachment_helpers.IsViewableImage(
+ 'image/gif; charset=binary', 123))
+ self.assertTrue(attachment_helpers.IsViewableImage('image/png', 123))
+ self.assertTrue(attachment_helpers.IsViewableImage(
+ 'image/png; charset=binary', 123))
+ self.assertTrue(attachment_helpers.IsViewableImage('image/x-png', 123))
+ self.assertTrue(attachment_helpers.IsViewableImage('image/jpeg', 123))
+ self.assertTrue(attachment_helpers.IsViewableImage(
+ 'image/jpeg; charset=binary', 123))
+ self.assertTrue(attachment_helpers.IsViewableImage(
+ 'image/jpeg', 14 * 1024 * 1024))
+
+ self.assertFalse(attachment_helpers.IsViewableImage('junk/bits', 123))
+ self.assertFalse(attachment_helpers.IsViewableImage(
+ 'junk/bits; charset=binary', 123))
+ self.assertFalse(attachment_helpers.IsViewableImage(
+ 'image/jpeg', 16 * 1024 * 1024))
+
+ def testIsViewableVideo(self):
+ self.assertTrue(attachment_helpers.IsViewableVideo('video/ogg', 123))
+ self.assertTrue(attachment_helpers.IsViewableVideo(
+ 'video/ogg; charset=binary', 123))
+ self.assertTrue(attachment_helpers.IsViewableVideo('video/mp4', 123))
+ self.assertTrue(attachment_helpers.IsViewableVideo(
+ 'video/mp4; charset=binary', 123))
+ self.assertTrue(attachment_helpers.IsViewableVideo('video/mpg', 123))
+ self.assertTrue(attachment_helpers.IsViewableVideo(
+ 'video/mpg; charset=binary', 123))
+ self.assertTrue(attachment_helpers.IsViewableVideo('video/mpeg', 123))
+ self.assertTrue(attachment_helpers.IsViewableVideo(
+ 'video/mpeg; charset=binary', 123))
+ self.assertTrue(attachment_helpers.IsViewableVideo(
+ 'video/mpeg', 14 * 1024 * 1024))
+
+ self.assertFalse(attachment_helpers.IsViewableVideo('junk/bits', 123))
+ self.assertFalse(attachment_helpers.IsViewableVideo(
+ 'junk/bits; charset=binary', 123))
+ self.assertFalse(attachment_helpers.IsViewableVideo(
+ 'video/mp4', 16 * 1024 * 1024))
+
+ def testIsViewableText(self):
+ self.assertTrue(attachment_helpers.IsViewableText('text/plain', 0))
+ self.assertTrue(attachment_helpers.IsViewableText('text/plain', 1000))
+ self.assertTrue(attachment_helpers.IsViewableText('text/html', 1000))
+ self.assertFalse(
+ attachment_helpers.IsViewableText('text/plain', 200 * 1024 * 1024))
+ self.assertFalse(attachment_helpers.IsViewableText('image/jpeg', 200))
+ self.assertFalse(
+ attachment_helpers.IsViewableText('image/jpeg', 200 * 1024 * 1024))
+
+ def testSignAttachmentID(self):
+ pass # TODO(jrobbins): write tests
+
+ @patch('tracker.attachment_helpers.SignAttachmentID')
+ def testGetDownloadURL(self, mock_SignAttachmentID):
+ """The download URL is always our to attachment servlet."""
+ mock_SignAttachmentID.return_value = 67890
+ self.assertEqual(
+ 'attachment?aid=12345&signed_aid=67890',
+ attachment_helpers.GetDownloadURL(12345))
+
+ def testGetViewURL(self):
+ """The view URL may add &inline=1, or use our text attachment servlet."""
+ attach = tracker_pb2.Attachment(
+ attachment_id=1, mimetype='see below', filesize=1000)
+ download_url = 'attachment?aid=1&signed_aid=2'
+
+ # Viewable image.
+ attach.mimetype = 'image/jpeg'
+ self.assertEqual(
+ download_url + '&inline=1',
+ attachment_helpers.GetViewURL(attach, download_url, 'proj'))
+
+ # Viewable video.
+ attach.mimetype = 'video/mpeg'
+ self.assertEqual(
+ download_url + '&inline=1',
+ attachment_helpers.GetViewURL(attach, download_url, 'proj'))
+
+ # Viewable text file.
+ attach.mimetype = 'text/html'
+ self.assertEqual(
+ '/p/proj/issues/attachmentText?aid=1',
+ attachment_helpers.GetViewURL(attach, download_url, 'proj'))
+
+ # Something we don't support.
+ attach.mimetype = 'audio/mp3'
+ self.assertIsNone(
+ attachment_helpers.GetViewURL(attach, download_url, 'proj'))
+
+ def testGetThumbnailURL(self):
+ """The thumbnail URL may add param thumb=1 or not."""
+ attach = tracker_pb2.Attachment(
+ attachment_id=1, mimetype='see below', filesize=1000)
+ download_url = 'attachment?aid=1&signed_aid=2'
+
+ # Viewable image.
+ attach.mimetype = 'image/jpeg'
+ self.assertEqual(
+ download_url + '&inline=1&thumb=1',
+ attachment_helpers.GetThumbnailURL(attach, download_url))
+
+ # Viewable video.
+ attach.mimetype = 'video/mpeg'
+ self.assertIsNone(
+ # Video thumbs are displayed via GetVideoURL rather than this.
+ attachment_helpers.GetThumbnailURL(attach, download_url))
+
+ # Something that we don't thumbnail.
+ attach.mimetype = 'audio/mp3'
+ self.assertIsNone(attachment_helpers.GetThumbnailURL(attach, download_url))
+
+ def testGetVideoURL(self):
+ """The video URL is the same as the view URL for actual videos."""
+ attach = tracker_pb2.Attachment(
+ attachment_id=1, mimetype='see below', filesize=1000)
+ download_url = 'attachment?aid=1&signed_aid=2'
+
+ # Viewable video.
+ attach.mimetype = 'video/mpeg'
+ self.assertEqual(
+ download_url + '&inline=1',
+ attachment_helpers.GetVideoURL(attach, download_url))
+
+ # Anything that is not a video.
+ attach.mimetype = 'audio/mp3'
+ self.assertIsNone(attachment_helpers.GetVideoURL(attach, download_url))
+
diff --git a/tracker/test/component_helpers_test.py b/tracker/test/component_helpers_test.py
new file mode 100644
index 0000000..ee7f56c
--- /dev/null
+++ b/tracker/test/component_helpers_test.py
@@ -0,0 +1,113 @@
+# Copyright 2016 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
+
+"""Unit tests for the component_helpers module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from tracker import component_helpers
+from tracker import tracker_bizobj
+
+
+class ComponentHelpersTest(unittest.TestCase):
+
+ def setUp(self):
+ self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ self.cd1 = tracker_bizobj.MakeComponentDef(
+ 1, 789, 'FrontEnd', 'doc', False, [], [111], 0, 0)
+ self.cd2 = tracker_bizobj.MakeComponentDef(
+ 2, 789, 'FrontEnd>Splash', 'doc', False, [], [222], 0, 0)
+ self.cd3 = tracker_bizobj.MakeComponentDef(
+ 3, 789, 'BackEnd', 'doc', True, [], [111, 333], 0, 0)
+ self.config.component_defs = [self.cd1, self.cd2, self.cd3]
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ config=fake.ConfigService())
+ self.services.user.TestAddUser('a@example.com', 111)
+ self.services.user.TestAddUser('b@example.com', 222)
+ self.services.user.TestAddUser('c@example.com', 333)
+ self.mr = fake.MonorailRequest(self.services)
+ self.mr.cnxn = fake.MonorailConnection()
+
+ def testParseComponentRequest_Empty(self):
+ post_data = fake.PostData(admins=[''], cc=[''], labels=[''])
+ parsed = component_helpers.ParseComponentRequest(
+ self.mr, post_data, self.services)
+ self.assertEqual('', parsed.leaf_name)
+ self.assertEqual('', parsed.docstring)
+ self.assertEqual([], parsed.admin_usernames)
+ self.assertEqual([], parsed.cc_usernames)
+ self.assertEqual([], parsed.admin_ids)
+ self.assertEqual([], parsed.cc_ids)
+ self.assertFalse(self.mr.errors.AnyErrors())
+
+ def testParseComponentRequest_Normal(self):
+ post_data = fake.PostData(
+ leaf_name=['FrontEnd'],
+ docstring=['The server-side app that serves pages'],
+ deprecated=[False],
+ admins=['a@example.com'],
+ cc=['b@example.com, c@example.com'],
+ labels=['Hot, Cold'])
+ parsed = component_helpers.ParseComponentRequest(
+ self.mr, post_data, self.services)
+ self.assertEqual('FrontEnd', parsed.leaf_name)
+ self.assertEqual('The server-side app that serves pages', parsed.docstring)
+ self.assertEqual(['a@example.com'], parsed.admin_usernames)
+ self.assertEqual(['b@example.com', 'c@example.com'], parsed.cc_usernames)
+ self.assertEqual(['Hot', 'Cold'], parsed.label_strs)
+ self.assertEqual([111], parsed.admin_ids)
+ self.assertEqual([222, 333], parsed.cc_ids)
+ self.assertEqual([0, 1], parsed.label_ids)
+ self.assertFalse(self.mr.errors.AnyErrors())
+
+ def testParseComponentRequest_InvalidUser(self):
+ post_data = fake.PostData(
+ leaf_name=['FrontEnd'],
+ docstring=['The server-side app that serves pages'],
+ deprecated=[False],
+ admins=['a@example.com, invalid_user'],
+ cc=['b@example.com, c@example.com'],
+ labels=[''])
+ parsed = component_helpers.ParseComponentRequest(
+ self.mr, post_data, self.services)
+ self.assertEqual('FrontEnd', parsed.leaf_name)
+ self.assertEqual('The server-side app that serves pages', parsed.docstring)
+ self.assertEqual(['a@example.com', 'invalid_user'], parsed.admin_usernames)
+ self.assertEqual(['b@example.com', 'c@example.com'], parsed.cc_usernames)
+ self.assertEqual([111], parsed.admin_ids)
+ self.assertEqual([222, 333], parsed.cc_ids)
+ self.assertTrue(self.mr.errors.AnyErrors())
+ self.assertEqual('invalid_user unrecognized', self.mr.errors.member_admins)
+
+ def testGetComponentCcIDs(self):
+ issue = tracker_pb2.Issue()
+ issues_components_cc_ids = component_helpers.GetComponentCcIDs(
+ issue, self.config)
+ self.assertEqual(set(), issues_components_cc_ids)
+
+ issue.component_ids = [1, 2]
+ issues_components_cc_ids = component_helpers.GetComponentCcIDs(
+ issue, self.config)
+ self.assertEqual({111, 222}, issues_components_cc_ids)
+
+ def testGetCcIDsForComponentAndAncestors(self):
+ components_cc_ids = component_helpers.GetCcIDsForComponentAndAncestors(
+ self.config, self.cd1)
+ self.assertEqual({111}, components_cc_ids)
+
+ components_cc_ids = component_helpers.GetCcIDsForComponentAndAncestors(
+ self.config, self.cd2)
+ self.assertEqual({111, 222}, components_cc_ids)
+
+ components_cc_ids = component_helpers.GetCcIDsForComponentAndAncestors(
+ self.config, self.cd3)
+ self.assertEqual({111, 333}, components_cc_ids)
diff --git a/tracker/test/componentcreate_test.py b/tracker/test/componentcreate_test.py
new file mode 100644
index 0000000..1325d9b
--- /dev/null
+++ b/tracker/test/componentcreate_test.py
@@ -0,0 +1,143 @@
+# Copyright 2016 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
+
+"""Unit tests for the componentcreate servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from framework import permissions
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import componentcreate
+from tracker import tracker_bizobj
+
+import webapp2
+
+
+class ComponentCreateTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ config=fake.ConfigService(),
+ project=fake.ProjectService())
+ self.servlet = componentcreate.ComponentCreate(
+ 'req', 'res', services=self.services)
+ self.project = self.services.project.TestAddProject('proj')
+ self.mr = testing_helpers.MakeMonorailRequest(
+ project=self.project, perms=permissions.OWNER_ACTIVE_PERMISSIONSET)
+ self.mr.auth.email = 'b@example.com'
+ self.config = self.services.config.GetProjectConfig(
+ 'fake cnxn', self.project.project_id)
+ self.services.config.StoreConfig('fake cnxn', self.config)
+ self.cd = tracker_bizobj.MakeComponentDef(
+ 1, self.project.project_id, 'BackEnd', 'doc', False, [], [111], 0,
+ 122)
+ self.config.component_defs = [self.cd]
+ self.services.user.TestAddUser('a@example.com', 111)
+ self.services.user.TestAddUser('b@example.com', 122)
+
+ def testAssertBasePermission(self):
+ # Anon users can never do it
+ self.mr.perms = permissions.READ_ONLY_PERMISSIONSET
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, self.mr)
+
+ # Project owner can do it.
+ self.mr.perms = permissions.OWNER_ACTIVE_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+
+ # Project member cannot do it
+ self.mr.perms = permissions.COMMITTER_ACTIVE_PERMISSIONSET
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, self.mr)
+ self.mr.perms = permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, self.mr)
+
+ def testGatherPageData_CreatingAtTopLevel(self):
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual(self.servlet.PROCESS_TAB_COMPONENTS,
+ page_data['admin_tab_mode'])
+ self.assertIsNone(page_data['parent_path'])
+
+ def testGatherPageData_CreatingASubComponent(self):
+ self.mr.component_path = 'BackEnd'
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual(self.servlet.PROCESS_TAB_COMPONENTS,
+ page_data['admin_tab_mode'])
+ self.assertEqual('BackEnd', page_data['parent_path'])
+
+ def testProcessFormData_NotFound(self):
+ post_data = fake.PostData(
+ parent_path=['Monitoring'],
+ leaf_name=['Rules'],
+ docstring=['Detecting outages'],
+ deprecated=[False],
+ admins=[''],
+ cc=[''],
+ labels=[''])
+ self.assertRaises(
+ webapp2.HTTPException,
+ self.servlet.ProcessFormData, self.mr, post_data)
+
+ def testProcessFormData_Normal(self):
+ post_data = fake.PostData(
+ parent_path=['BackEnd'],
+ leaf_name=['DB'],
+ docstring=['A database'],
+ deprecated=[False],
+ admins=[''],
+ cc=[''],
+ labels=[''])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertTrue('/adminComponents?saved=1&' in url)
+ config = self.services.config.GetProjectConfig(
+ self.mr.cnxn, self.mr.project_id)
+
+ cd = tracker_bizobj.FindComponentDef('BackEnd>DB', config)
+ self.assertEqual('BackEnd>DB', cd.path)
+ self.assertEqual('A database', cd.docstring)
+ self.assertEqual([], cd.admin_ids)
+ self.assertEqual([], cd.cc_ids)
+ self.assertTrue(cd.created > 0)
+ self.assertEqual(122, cd.creator_id)
+
+
+class ComponentCreateMethodsTest(unittest.TestCase):
+
+ def setUp(self):
+ self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ cd1 = tracker_bizobj.MakeComponentDef(
+ 1, 789, 'BackEnd', 'doc', False, [], [111], 0, 122)
+ cd2 = tracker_bizobj.MakeComponentDef(
+ 2, 789, 'BackEnd>DB', 'doc', True, [], [111], 0, 122)
+ self.config.component_defs = [cd1, cd2]
+
+ def testLeafNameErrorMessage_Invalid(self):
+ self.assertEqual(
+ 'Invalid component name',
+ componentcreate.LeafNameErrorMessage('', 'bad name', self.config))
+
+ def testLeafNameErrorMessage_AlreadyInUse(self):
+ self.assertEqual(
+ 'That name is already in use.',
+ componentcreate.LeafNameErrorMessage('', 'BackEnd', self.config))
+ self.assertEqual(
+ 'That name is already in use.',
+ componentcreate.LeafNameErrorMessage('BackEnd', 'DB', self.config))
+
+ def testLeafNameErrorMessage_OK(self):
+ self.assertIsNone(
+ componentcreate.LeafNameErrorMessage('', 'FrontEnd', self.config))
+ self.assertIsNone(
+ componentcreate.LeafNameErrorMessage('BackEnd', 'Search', self.config))
diff --git a/tracker/test/componentdetail_test.py b/tracker/test/componentdetail_test.py
new file mode 100644
index 0000000..18886bc
--- /dev/null
+++ b/tracker/test/componentdetail_test.py
@@ -0,0 +1,320 @@
+# Copyright 2016 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
+
+"""Unit tests for the componentdetail servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from mock import Mock, patch
+
+import mox
+
+from features import filterrules_helpers
+from framework import permissions
+from proto import project_pb2
+from services import service_manager
+from services import template_svc
+from testing import fake
+from testing import testing_helpers
+from tracker import componentdetail
+from tracker import tracker_bizobj
+
+import webapp2
+
+
+class ComponentDetailTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ issue=fake.IssueService(),
+ config=fake.ConfigService(),
+ template=Mock(spec=template_svc.TemplateService),
+ project=fake.ProjectService())
+ self.servlet = componentdetail.ComponentDetail(
+ 'req', 'res', services=self.services)
+ self.project = self.services.project.TestAddProject('proj')
+ self.mr = testing_helpers.MakeMonorailRequest(
+ project=self.project, perms=permissions.OWNER_ACTIVE_PERMISSIONSET)
+ self.mr.auth.email = 'b@example.com'
+ self.config = self.services.config.GetProjectConfig(
+ 'fake cnxn', self.project.project_id)
+ self.services.config.StoreConfig('fake cnxn', self.config)
+ self.cd = tracker_bizobj.MakeComponentDef(
+ 1, self.project.project_id, 'BackEnd', 'doc', False, [], [111], 100000,
+ 122, 10000000, 133)
+ self.config.component_defs = [self.cd]
+ self.services.user.TestAddUser('a@example.com', 111)
+ self.services.user.TestAddUser('b@example.com', 122)
+ self.services.user.TestAddUser('c@example.com', 133)
+ self.mr.component_path = 'BackEnd'
+
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testGetComponentDef_NotFound(self):
+ self.mr.component_path = 'NeverHeardOfIt'
+ self.assertRaises(
+ webapp2.HTTPException,
+ self.servlet._GetComponentDef, self.mr)
+
+ def testGetComponentDef_Normal(self):
+ actual_config, actual_cd = self.servlet._GetComponentDef(self.mr)
+ self.assertEqual(self.config, actual_config)
+ self.assertEqual(self.cd, actual_cd)
+
+ def testAssertBasePermission_AnyoneCanView(self):
+ self.servlet.AssertBasePermission(self.mr)
+ self.mr.perms = permissions.COMMITTER_ACTIVE_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+ self.mr.perms = permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+ self.mr.perms = permissions.READ_ONLY_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+
+ def testAssertBasePermission_MembersOnly(self):
+ self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ # The project members can view the component definition.
+ self.servlet.AssertBasePermission(self.mr)
+ self.mr.perms = permissions.COMMITTER_ACTIVE_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+ self.mr.perms = permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+ # Non-member is not allowed to view anything in the project.
+ self.mr.perms = permissions.EMPTY_PERMISSIONSET
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, self.mr)
+
+ def testGatherPageData_ReadWrite(self):
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual(self.servlet.PROCESS_TAB_COMPONENTS,
+ page_data['admin_tab_mode'])
+ self.assertTrue(page_data['allow_edit'])
+ self.assertEqual([], page_data['initial_admins'])
+ component_def_view = page_data['component_def']
+ self.assertEqual('BackEnd', component_def_view.path)
+
+ def testGatherPageData_ReadOnly(self):
+ self.mr.perms = permissions.READ_ONLY_PERMISSIONSET
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual(self.servlet.PROCESS_TAB_COMPONENTS,
+ page_data['admin_tab_mode'])
+ self.assertFalse(page_data['allow_edit'])
+ self.assertFalse(page_data['allow_delete'])
+ self.assertEqual([], page_data['initial_admins'])
+ component_def_view = page_data['component_def']
+ self.assertEqual('BackEnd', component_def_view.path)
+
+ def testGatherPageData_ObscuredCreatorModifier(self):
+ page_data = self.servlet.GatherPageData(self.mr)
+
+ self.assertEqual('b...@example.com', page_data['creator'].display_name)
+ self.assertEqual('/u/122/', page_data['creator'].profile_url)
+ self.assertEqual('Jan 1970', page_data['created'])
+ self.assertEqual('c...@example.com', page_data['modifier'].display_name)
+ self.assertEqual('/u/133/', page_data['modifier'].profile_url)
+ self.assertEqual('Apr 1970', page_data['modified'])
+
+ def testGatherPageData_VisibleCreatorModifierForAdmin(self):
+ self.mr.auth.user_pb.is_site_admin = True
+ page_data = self.servlet.GatherPageData(self.mr)
+
+ self.assertEqual('b@example.com', page_data['creator'].display_name)
+ self.assertEqual('/u/b@example.com/', page_data['creator'].profile_url)
+ self.assertEqual('Jan 1970', page_data['created'])
+ self.assertEqual('c@example.com', page_data['modifier'].display_name)
+ self.assertEqual('/u/c@example.com/', page_data['modifier'].profile_url)
+ self.assertEqual('Apr 1970', page_data['modified'])
+
+ def testGatherPageData_VisibleCreatorForSelf(self):
+ self.mr.auth.user_id = 122
+ page_data = self.servlet.GatherPageData(self.mr)
+
+ self.assertEqual('b@example.com', page_data['creator'].display_name)
+ self.assertEqual('/u/b@example.com/', page_data['creator'].profile_url)
+ self.assertEqual('Jan 1970', page_data['created'])
+ # Modifier should still be obscured.
+ self.assertEqual('c...@example.com', page_data['modifier'].display_name)
+ self.assertEqual('/u/133/', page_data['modifier'].profile_url)
+ self.assertEqual('Apr 1970', page_data['modified'])
+
+ def testGatherPageData_VisibleCreatorModifierForUnobscuredEmail(self):
+ creator = self.services.user.GetUser(self.mr.cnxn, 122)
+ creator.obscure_email = False
+ modifier = self.services.user.GetUser(self.mr.cnxn, 133)
+ modifier.obscure_email = False
+ page_data = self.servlet.GatherPageData(self.mr)
+
+ self.assertEqual('b@example.com', page_data['creator'].display_name)
+ self.assertEqual('/u/b@example.com/', page_data['creator'].profile_url)
+ self.assertEqual('Jan 1970', page_data['created'])
+ self.assertEqual('c@example.com', page_data['modifier'].display_name)
+ self.assertEqual('/u/c@example.com/', page_data['modifier'].profile_url)
+ self.assertEqual('Apr 1970', page_data['modified'])
+
+ def testGatherPageData_WithSubComponents(self):
+ subcd = tracker_bizobj.MakeComponentDef(
+ 2, self.project.project_id, 'BackEnd>Worker', 'doc', False, [], [111],
+ 0, 122)
+ self.config.component_defs.append(subcd)
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertFalse(page_data['allow_delete'])
+ self.assertEqual([subcd], page_data['subcomponents'])
+
+ def testGatherPageData_WithTemplates(self):
+ self.services.template.TemplatesWithComponent.return_value = ['template']
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertFalse(page_data['allow_delete'])
+ self.assertEqual(['template'], page_data['templates'])
+
+ def testProcessFormData_Permission(self):
+ """Only owners can edit components."""
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET)
+ mr.component_path = 'BackEnd'
+ post_data = fake.PostData(
+ name=['BackEnd'],
+ deletecomponent=['Submit'])
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.ProcessFormData, mr, post_data)
+
+ self.servlet.ProcessFormData(self.mr, post_data)
+
+ def testProcessFormData_Delete(self):
+ post_data = fake.PostData(
+ name=['BackEnd'],
+ deletecomponent=['Submit'])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertTrue('/adminComponents?deleted=1&' in url)
+ self.assertIsNone(
+ tracker_bizobj.FindComponentDef('BackEnd', self.config))
+
+ def testProcessFormData_Delete_WithSubComponent(self):
+ subcd = tracker_bizobj.MakeComponentDef(
+ 2, self.project.project_id, 'BackEnd>Worker', 'doc', False, [], [111],
+ 0, 122)
+ self.config.component_defs.append(subcd)
+
+ post_data = fake.PostData(
+ name=['BackEnd'],
+ deletecomponent=['Submit'])
+ with self.assertRaises(permissions.PermissionException) as cm:
+ self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertEqual(
+ 'User tried to delete component that had subcomponents',
+ cm.exception.message)
+
+ def testProcessFormData_Edit(self):
+ post_data = fake.PostData(
+ leaf_name=['BackEnd'],
+ docstring=['This is where the magic happens'],
+ deprecated=[True],
+ admins=['a@example.com'],
+ cc=['a@example.com'],
+ labels=['Hot, Cold'])
+
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+
+ self.mox.VerifyAll()
+ self.assertTrue('/components/detail?component=BackEnd&saved=1&' in url)
+ config = self.services.config.GetProjectConfig(
+ self.mr.cnxn, self.mr.project_id)
+
+ cd = tracker_bizobj.FindComponentDef('BackEnd', config)
+ self.assertEqual('BackEnd', cd.path)
+ self.assertEqual(
+ 'This is where the magic happens',
+ cd.docstring)
+ self.assertEqual(True, cd.deprecated)
+ self.assertEqual([111], cd.admin_ids)
+ self.assertEqual([111], cd.cc_ids)
+
+ def testProcessDeleteComponent(self):
+ self.servlet._ProcessDeleteComponent(self.mr, self.cd)
+ self.assertIsNone(
+ tracker_bizobj.FindComponentDef('BackEnd', self.config))
+
+ def testProcessEditComponent(self):
+ post_data = fake.PostData(
+ leaf_name=['BackEnd'],
+ docstring=['This is where the magic happens'],
+ deprecated=[True],
+ admins=['a@example.com'],
+ cc=['a@example.com'],
+ labels=['Hot, Cold'])
+
+ self.servlet._ProcessEditComponent(
+ self.mr, post_data, self.config, self.cd)
+
+ self.mox.VerifyAll()
+ config = self.services.config.GetProjectConfig(
+ self.mr.cnxn, self.mr.project_id)
+ cd = tracker_bizobj.FindComponentDef('BackEnd', config)
+ self.assertEqual('BackEnd', cd.path)
+ self.assertEqual(
+ 'This is where the magic happens',
+ cd.docstring)
+ self.assertEqual(True, cd.deprecated)
+ self.assertEqual([111], cd.admin_ids)
+ self.assertEqual([111], cd.cc_ids)
+ # Assert that creator and created were not updated.
+ self.assertEqual(122, cd.creator_id)
+ self.assertEqual(100000, cd.created)
+ # Assert that modifier and modified were updated.
+ self.assertEqual(122, cd.modifier_id)
+ self.assertTrue(cd.modified > 10000000)
+
+ def testProcessEditComponent_RenameWithSubComponents(self):
+ subcd_1 = tracker_bizobj.MakeComponentDef(
+ 2, self.project.project_id, 'BackEnd>Worker1', 'doc', False, [], [111],
+ 0, 125, 3, 126)
+ subcd_2 = tracker_bizobj.MakeComponentDef(
+ 3, self.project.project_id, 'BackEnd>Worker2', 'doc', False, [], [111],
+ 0, 125, 4, 127)
+ self.config.component_defs.extend([subcd_1, subcd_2])
+
+ self.mox.StubOutWithMock(filterrules_helpers, 'RecomputeAllDerivedFields')
+ filterrules_helpers.RecomputeAllDerivedFields(
+ self.mr.cnxn, self.services, self.mr.project, self.config)
+ self.mox.ReplayAll()
+ post_data = fake.PostData(
+ leaf_name=['BackEnds'],
+ docstring=['This is where the magic happens'],
+ deprecated=[True],
+ admins=['a@example.com'],
+ cc=['a@example.com'],
+ labels=[''])
+
+ self.servlet._ProcessEditComponent(
+ self.mr, post_data, self.config, self.cd)
+
+ self.mox.VerifyAll()
+ config = self.services.config.GetProjectConfig(
+ self.mr.cnxn, self.mr.project_id)
+ cd = tracker_bizobj.FindComponentDef('BackEnds', config)
+ self.assertEqual('BackEnds', cd.path)
+ subcd_1 = tracker_bizobj.FindComponentDef('BackEnds>Worker1', config)
+ self.assertEqual('BackEnds>Worker1', subcd_1.path)
+ # Assert that creator and modifier have not changed for subcd_1.
+ self.assertEqual(125, subcd_1.creator_id)
+ self.assertEqual(0, subcd_1.created)
+ self.assertEqual(126, subcd_1.modifier_id)
+ self.assertEqual(3, subcd_1.modified)
+
+ subcd_2 = tracker_bizobj.FindComponentDef('BackEnds>Worker2', config)
+ self.assertEqual('BackEnds>Worker2', subcd_2.path)
+ # Assert that creator and modifier have not changed for subcd_2.
+ self.assertEqual(125, subcd_2.creator_id)
+ self.assertEqual(0, subcd_2.created)
+ self.assertEqual(127, subcd_2.modifier_id)
+ self.assertEqual(4, subcd_2.modified)
diff --git a/tracker/test/field_helpers_test.py b/tracker/test/field_helpers_test.py
new file mode 100644
index 0000000..f49a147
--- /dev/null
+++ b/tracker/test/field_helpers_test.py
@@ -0,0 +1,1276 @@
+# Copyright 2016 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
+
+"""Unit tests for the field_helpers module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import time
+import unittest
+import re
+
+from framework import exceptions
+from framework import permissions
+from framework import template_helpers
+from proto import project_pb2
+from proto import tracker_pb2
+from services import service_manager
+from services import config_svc
+from testing import fake
+from testing import testing_helpers
+from tracker import field_helpers
+from tracker import tracker_bizobj
+
+
+class FieldHelpersTest(unittest.TestCase):
+
+ def setUp(self):
+ self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ self.config.well_known_labels.append(tracker_pb2.LabelDef(
+ label='OldLabel', label_docstring='Do not use any longer',
+ deprecated=True))
+
+ self.services = service_manager.Services(
+ issue=fake.IssueService(),
+ usergroup=fake.UserGroupService(),
+ config=fake.ConfigService(),
+ user=fake.UserService())
+ self.project = fake.Project()
+ self.mr = testing_helpers.MakeMonorailRequest(
+ project=self.project, services=self.services)
+ self.mr.cnxn = fake.MonorailConnection()
+ self.errors = template_helpers.EZTError()
+
+ def testListApplicableFieldDefs(self):
+ issue_1 = fake.MakeTestIssue(
+ 789,
+ 1,
+ 'sum',
+ 'New',
+ 111,
+ issue_id=78901,
+ labels=['type-defect', 'other-label'])
+ issue_2 = fake.MakeTestIssue(
+ 789,
+ 2,
+ 'sum',
+ 'New',
+ 111,
+ issue_id=78902,
+ labels=['type-feedback', 'other-label1'])
+ issue_3 = fake.MakeTestIssue(
+ 789,
+ 3,
+ 'sum',
+ 'New',
+ 111,
+ issue_id=78903,
+ labels=['type-defect'],
+ approval_values=[
+ tracker_pb2.ApprovalValue(approval_id=3),
+ tracker_pb2.ApprovalValue(approval_id=5)
+ ])
+ issue_4 = fake.MakeTestIssue(
+ 789, 4, 'sum', 'New', 111, issue_id=78904) # test no labels at all
+ issue_5 = fake.MakeTestIssue(
+ 789,
+ 5,
+ 'sum',
+ 'New',
+ 111,
+ issue_id=78905,
+ labels=['type'], # test labels ignored
+ approval_values=[tracker_pb2.ApprovalValue(approval_id=5)])
+ self.services.issue.TestAddIssue(issue_1)
+ self.services.issue.TestAddIssue(issue_2)
+ self.services.issue.TestAddIssue(issue_3)
+ self.services.issue.TestAddIssue(issue_4)
+ self.services.issue.TestAddIssue(issue_5)
+ fd_1 = tracker_pb2.FieldDef(
+ field_name='FirstField',
+ field_id=1,
+ field_type=tracker_pb2.FieldTypes.STR_TYPE,
+ applicable_type='feedback') # applicable
+ fd_2 = tracker_pb2.FieldDef(
+ field_name='SecField',
+ field_id=2,
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ applicable_type='no') # not applicable
+ fd_3 = tracker_pb2.FieldDef(
+ field_name='LegalApproval',
+ field_id=3,
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ applicable_type='') # applicable
+ fd_4 = tracker_pb2.FieldDef(
+ field_name='UserField',
+ field_id=4,
+ field_type=tracker_pb2.FieldTypes.USER_TYPE,
+ applicable_type='') # applicable
+ fd_5 = tracker_pb2.FieldDef(
+ field_name='DogApproval',
+ field_id=5,
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ applicable_type='') # applicable
+ fd_6 = tracker_pb2.FieldDef(
+ field_name='SixthField',
+ field_id=6,
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ applicable_type='Defect') # applicable
+ fd_7 = tracker_pb2.FieldDef(
+ field_name='CatApproval',
+ field_id=7,
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ applicable_type='') # not applicable
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ config.field_defs = [fd_1, fd_2, fd_3, fd_4, fd_5, fd_6, fd_7]
+ issues = [issue_1, issue_2, issue_3, issue_4, issue_5]
+
+ actual_fds = field_helpers.ListApplicableFieldDefs(issues, config)
+ self.assertEqual(actual_fds, [fd_1, fd_3, fd_4, fd_5, fd_6])
+
+ def testParseFieldDefRequest_Empty(self):
+ post_data = fake.PostData()
+ parsed = field_helpers.ParseFieldDefRequest(post_data, self.config)
+ self.assertEqual('', parsed.field_name)
+ self.assertEqual(None, parsed.field_type_str)
+ self.assertEqual(None, parsed.min_value)
+ self.assertEqual(None, parsed.max_value)
+ self.assertEqual(None, parsed.regex)
+ self.assertFalse(parsed.needs_member)
+ self.assertEqual('', parsed.needs_perm)
+ self.assertEqual('', parsed.grants_perm)
+ self.assertEqual(0, parsed.notify_on)
+ self.assertFalse(parsed.is_required)
+ self.assertFalse(parsed.is_niche)
+ self.assertFalse(parsed.is_multivalued)
+ self.assertEqual('', parsed.field_docstring)
+ self.assertEqual('', parsed.choices_text)
+ self.assertEqual('', parsed.applicable_type)
+ self.assertEqual('', parsed.applicable_predicate)
+ unchanged_labels = [
+ (label_def.label, label_def.label_docstring, label_def.deprecated)
+ for label_def in self.config.well_known_labels]
+ self.assertEqual(unchanged_labels, parsed.revised_labels)
+ self.assertEqual('', parsed.approvers_str)
+ self.assertEqual('', parsed.survey)
+ self.assertEqual('', parsed.parent_approval_name)
+ self.assertFalse(parsed.is_phase_field)
+
+ def testParseFieldDefRequest_Normal(self):
+ post_data = fake.PostData(
+ name=['somefield'],
+ field_type=['INT_TYPE'],
+ min_value=['11'],
+ max_value=['99'],
+ regex=['.*'],
+ needs_member=['Yes'],
+ needs_perm=['Commit'],
+ grants_perm=['View'],
+ notify_on=['any_comment'],
+ importance=['required'],
+ is_multivalued=['Yes'],
+ docstring=['It is just some field'],
+ choices=['Hot = Lots of activity\nCold = Not much activity'],
+ applicable_type=['Defect'],
+ approver_names=['approver@chromium.org'],
+ survey=['Are there UX changes?'],
+ parent_approval_name=['UIReview'],
+ is_phase_field=['on'],
+ )
+ parsed = field_helpers.ParseFieldDefRequest(post_data, self.config)
+ self.assertEqual('somefield', parsed.field_name)
+ self.assertEqual('INT_TYPE', parsed.field_type_str)
+ self.assertEqual(11, parsed.min_value)
+ self.assertEqual(99, parsed.max_value)
+ self.assertEqual('.*', parsed.regex)
+ self.assertTrue(parsed.needs_member)
+ self.assertEqual('Commit', parsed.needs_perm)
+ self.assertEqual('View', parsed.grants_perm)
+ self.assertEqual(1, parsed.notify_on)
+ self.assertTrue(parsed.is_required)
+ self.assertFalse(parsed.is_niche)
+ self.assertTrue(parsed.is_multivalued)
+ self.assertEqual('It is just some field', parsed.field_docstring)
+ self.assertEqual('Hot = Lots of activity\nCold = Not much activity',
+ parsed.choices_text)
+ self.assertEqual('Defect', parsed.applicable_type)
+ self.assertEqual('', parsed.applicable_predicate)
+ unchanged_labels = [
+ (label_def.label, label_def.label_docstring, label_def.deprecated)
+ for label_def in self.config.well_known_labels]
+ new_labels = [
+ ('somefield-Hot', 'Lots of activity', False),
+ ('somefield-Cold', 'Not much activity', False)]
+ self.assertEqual(unchanged_labels + new_labels, parsed.revised_labels)
+ self.assertEqual('approver@chromium.org', parsed.approvers_str)
+ self.assertEqual('Are there UX changes?', parsed.survey)
+ self.assertEqual('UIReview', parsed.parent_approval_name)
+ self.assertTrue(parsed.is_phase_field)
+
+ def testParseFieldDefRequest_PreventPhaseApprovals(self):
+ post_data = fake.PostData(
+ field_type=['approval_type'],
+ is_phase_field=['on'],
+ )
+ parsed = field_helpers.ParseFieldDefRequest(post_data, self.config)
+ self.assertEqual('approval_type', parsed.field_type_str)
+ self.assertFalse(parsed.is_phase_field)
+
+ def testParseChoicesIntoWellKnownLabels_NewFieldDef(self):
+ choices_text = 'Hot = Lots of activity\nCold = Not much activity'
+ field_name = 'somefield'
+ revised_labels = field_helpers._ParseChoicesIntoWellKnownLabels(
+ choices_text, field_name, self.config, 'enum_type')
+ unchanged_labels = [
+ (label_def.label, label_def.label_docstring, label_def.deprecated)
+ for label_def in self.config.well_known_labels]
+ new_labels = [
+ ('somefield-Hot', 'Lots of activity', False),
+ ('somefield-Cold', 'Not much activity', False)]
+ self.assertEqual(unchanged_labels + new_labels, revised_labels)
+
+ def testParseChoicesIntoWellKnownLabels_ConvertExistingLabel(self):
+ choices_text = 'High = Must be fixed\nMedium = Might slip'
+ field_name = 'Priority'
+ revised_labels = field_helpers._ParseChoicesIntoWellKnownLabels(
+ choices_text, field_name, self.config, 'enum_type')
+ kept_labels = [
+ (label_def.label, label_def.label_docstring, label_def.deprecated)
+ for label_def in self.config.well_known_labels
+ if not label_def.label.startswith('Priority-')]
+ new_labels = [
+ ('Priority-High', 'Must be fixed', False),
+ ('Priority-Medium', 'Might slip', False)]
+ self.maxDiff = None
+ self.assertEqual(kept_labels + new_labels, revised_labels)
+
+ # TODO(jojwang): test this separately
+ # test None field_type_str, updating existing fielddef
+ self.config.field_defs.append(tracker_pb2.FieldDef(
+ field_id=13, field_name='Priority',
+ field_type=tracker_pb2.FieldTypes.ENUM_TYPE))
+ revised_labels = field_helpers._ParseChoicesIntoWellKnownLabels(
+ choices_text, field_name, self.config, None)
+ self.assertEqual(kept_labels + new_labels, revised_labels)
+
+ def testParseChoicesIntoWellKnownLabels_NotEnumField(self):
+ choices_text = ''
+ field_name = 'NotEnum'
+ self.config.well_known_labels = [
+ tracker_pb2.LabelDef(label='NotEnum-Should'),
+ tracker_pb2.LabelDef(label='NotEnum-Not-Be-Masked')]
+ revised_labels = field_helpers._ParseChoicesIntoWellKnownLabels(
+ choices_text, field_name, self.config, 'str_type')
+ new_labels = [
+ ('NotEnum-Should', None, False),
+ ('NotEnum-Not-Be-Masked', None, False)]
+ self.assertEqual(new_labels, revised_labels)
+
+ # TODO(jojwang): test this separately
+ # test None field_type_str, updating existing fielddef
+ self.config.field_defs.append(tracker_pb2.FieldDef(
+ field_id=13, field_name='NotEnum',
+ field_type=tracker_pb2.FieldTypes.STR_TYPE))
+ revised_labels = field_helpers._ParseChoicesIntoWellKnownLabels(
+ choices_text, field_name, self.config, None)
+ self.assertEqual(revised_labels, new_labels)
+
+ def testShiftEnumFieldsIntoLabels_Empty(self):
+ labels = []
+ labels_remove = []
+ field_val_strs = {}
+ field_val_strs_remove = {}
+ field_helpers.ShiftEnumFieldsIntoLabels(
+ labels, labels_remove, field_val_strs, field_val_strs_remove,
+ self.config)
+ self.assertEqual([], labels)
+ self.assertEqual([], labels_remove)
+ self.assertEqual({}, field_val_strs)
+ self.assertEqual({}, field_val_strs_remove)
+
+ def testShiftEnumFieldsIntoLabels_NoOp(self):
+ labels = ['Security', 'Performance', 'Pri-1', 'M-2']
+ labels_remove = ['ReleaseBlock']
+ field_val_strs = {123: ['CPU']}
+ field_val_strs_remove = {234: ['Small']}
+ field_helpers.ShiftEnumFieldsIntoLabels(
+ labels, labels_remove, field_val_strs, field_val_strs_remove,
+ self.config)
+ self.assertEqual(['Security', 'Performance', 'Pri-1', 'M-2'], labels)
+ self.assertEqual(['ReleaseBlock'], labels_remove)
+ self.assertEqual({123: ['CPU']}, field_val_strs)
+ self.assertEqual({234: ['Small']}, field_val_strs_remove)
+
+ def testShiftEnumFieldsIntoLabels_FoundSomeEnumFields(self):
+ self.config.field_defs.append(
+ tracker_bizobj.MakeFieldDef(
+ 123, 789, 'Component', tracker_pb2.FieldTypes.ENUM_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action', 'What HW part is affected?',
+ False))
+ self.config.field_defs.append(
+ tracker_bizobj.MakeFieldDef(
+ 234, 789, 'Size', tracker_pb2.FieldTypes.ENUM_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action', 'How big is this work item?',
+ False))
+ labels = ['Security', 'Performance', 'Pri-1', 'M-2']
+ labels_remove = ['ReleaseBlock']
+ field_val_strs = {123: ['CPU']}
+ field_val_strs_remove = {234: ['Small']}
+ field_helpers.ShiftEnumFieldsIntoLabels(
+ labels, labels_remove, field_val_strs, field_val_strs_remove,
+ self.config)
+ self.assertEqual(
+ ['Security', 'Performance', 'Pri-1', 'M-2', 'Component-CPU'],
+ labels)
+ self.assertEqual(['ReleaseBlock', 'Size-Small'], labels_remove)
+ self.assertEqual({}, field_val_strs)
+ self.assertEqual({}, field_val_strs_remove)
+
+ def testReviseApprovals_New(self):
+ self.config.field_defs.append(
+ tracker_bizobj.MakeFieldDef(
+ 123, 789, 'UX Review', tracker_pb2.FieldTypes.APPROVAL_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'Approval for UX review', False))
+ existing_approvaldef = tracker_pb2.ApprovalDef(
+ approval_id=123, approver_ids=[101, 102], survey='')
+ self.config.approval_defs = [existing_approvaldef]
+ revised_approvals = field_helpers.ReviseApprovals(
+ 124, [103], '', self.config)
+ self.assertEqual(len(revised_approvals), 2)
+ self.assertEqual(revised_approvals,
+ [(123, [101, 102], ''), (124, [103], '')])
+
+ def testReviseApprovals_Existing(self):
+ existing_approvaldef = tracker_pb2.ApprovalDef(
+ approval_id=123, approver_ids=[101, 102], survey='')
+ self.config.approval_defs = [existing_approvaldef]
+ revised_approvals = field_helpers.ReviseApprovals(
+ 123, [103], '', self.config)
+ self.assertEqual(revised_approvals, [(123, [103], '')])
+
+ def testParseOneFieldValue_IntType(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'Foo', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ fv = field_helpers.ParseOneFieldValue(
+ self.mr.cnxn, self.services.user, fd, '8675309')
+ self.assertEqual(fv.field_id, 123)
+ self.assertEqual(fv.int_value, 8675309)
+
+ def testParseOneFieldValue_StrType(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'Foo', tracker_pb2.FieldTypes.STR_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ fv = field_helpers.ParseOneFieldValue(
+ self.mr.cnxn, self.services.user, fd, '8675309')
+ self.assertEqual(fv.field_id, 123)
+ self.assertEqual(fv.str_value, '8675309')
+
+ def testParseOneFieldValue_UserType(self):
+ self.services.user.TestAddUser('user@example.com', 111)
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'Foo', tracker_pb2.FieldTypes.USER_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ fv = field_helpers.ParseOneFieldValue(
+ self.mr.cnxn, self.services.user, fd, 'user@example.com')
+ self.assertEqual(fv.field_id, 123)
+ self.assertEqual(fv.user_id, 111)
+
+ def testParseOneFieldValue_DateType(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'Deadline', tracker_pb2.FieldTypes.DATE_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ fv = field_helpers.ParseOneFieldValue(
+ self.mr.cnxn, self.services.user, fd, '2009-02-13')
+ self.assertEqual(fv.field_id, 123)
+ self.assertEqual(fv.date_value, 1234483200)
+
+ def testParseOneFieldValue_UrlType(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'Design Doc', tracker_pb2.FieldTypes.URL_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ fv = field_helpers.ParseOneFieldValue(
+ self.mr.cnxn, self.services.user, fd, 'www.google.com')
+ self.assertEqual(fv.field_id, 123)
+ self.assertEqual(fv.url_value, 'http://www.google.com')
+
+ def testParseOneFieldValue(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'Target', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'milestone target',
+ False, is_phase_field=True)
+ phase_fvs = field_helpers.ParseOnePhaseFieldValue(
+ self.mr.cnxn, self.services.user, fd, '70', [30, 40])
+ self.assertEqual(len(phase_fvs), 2)
+ self.assertEqual(phase_fvs[0].phase_id, 30)
+ self.assertEqual(phase_fvs[1].phase_id, 40)
+
+ def testParseFieldValues_Empty(self):
+ field_val_strs = {}
+ phase_field_val_strs = {}
+ field_values = field_helpers.ParseFieldValues(
+ self.mr.cnxn, self.services.user, field_val_strs,
+ phase_field_val_strs, self.config)
+ self.assertEqual([], field_values)
+
+ def testParseFieldValues_EmptyPhases(self):
+ field_val_strs = {126: ['70']}
+ phase_field_val_strs = {}
+ fd_phase = tracker_bizobj.MakeFieldDef(
+ 126, 789, 'Target', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'milestone target',
+ False, is_phase_field=True)
+ self.config.field_defs.extend([fd_phase])
+ field_values = field_helpers.ParseFieldValues(
+ self.mr.cnxn, self.services.user, field_val_strs,
+ phase_field_val_strs, self.config)
+ self.assertEqual([], field_values)
+
+ def testParseFieldValues_Normal(self):
+ fd_int = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ fd_date = tracker_bizobj.MakeFieldDef(
+ 124, 789, 'Deadline', tracker_pb2.FieldTypes.DATE_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ fd_url = tracker_bizobj.MakeFieldDef(
+ 125, 789, 'Design Doc', tracker_pb2.FieldTypes.URL_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ fd_phase = tracker_bizobj.MakeFieldDef(
+ 126, 789, 'Target', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'milestone target',
+ False, is_phase_field=True)
+ self.config.field_defs.extend([fd_int, fd_date, fd_url, fd_phase])
+ field_val_strs = {
+ 123: ['80386', '68040'],
+ 124: ['2009-02-13'],
+ 125: ['www.google.com'],
+ }
+ phase_field_val_strs = {
+ 126: {'beta': ['89'],
+ 'stable': ['70'],
+ 'missing': ['234'],
+ }}
+ field_values = field_helpers.ParseFieldValues(
+ self.mr.cnxn, self.services.user, field_val_strs,
+ phase_field_val_strs, self.config,
+ phase_ids_by_name={'stable': [30, 40], 'beta': [88]})
+ fv1 = tracker_bizobj.MakeFieldValue(
+ 123, 80386, None, None, None, None, False)
+ fv2 = tracker_bizobj.MakeFieldValue(
+ 123, 68040, None, None, None, None, False)
+ fv3 = tracker_bizobj.MakeFieldValue(
+ 124, None, None, None, 1234483200, None, False)
+ fv4 = tracker_bizobj.MakeFieldValue(
+ 125, None, None, None, None, 'http://www.google.com', False)
+ fv5 = tracker_bizobj.MakeFieldValue(
+ 126, 89, None, None, None, None, False, phase_id=88)
+ fv6 = tracker_bizobj.MakeFieldValue(
+ 126, 70, None, None, None, None, False, phase_id=30)
+ fv7 = tracker_bizobj.MakeFieldValue(
+ 126, 70, None, None, None, None, False, phase_id=40)
+ self.assertEqual([fv1, fv2, fv3, fv4, fv5, fv6, fv7], field_values)
+
+ def test_IntType(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ fv = tracker_bizobj.MakeFieldValue(123, 8086, None, None, None, None, False)
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, fv)
+ self.assertIsNone(msg)
+
+ fd.min_value = 1
+ fd.max_value = 999
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, fv)
+ self.assertEqual('Value must be <= 999.', msg)
+
+ fv.int_value = 0
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, fv)
+ self.assertEqual('Value must be >= 1.', msg)
+
+ def test_StrType(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.STR_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ fv = tracker_bizobj.MakeFieldValue(
+ 123, None, 'i386', None, None, None, False)
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, fv)
+ self.assertIsNone(msg)
+
+ fd.regex = r'^\d*$'
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, fv)
+ self.assertEqual(r'Value must match regular expression: ^\d*$.', msg)
+
+ fv.str_value = '386'
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, fv)
+ self.assertIsNone(msg)
+
+ def test_UserType(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'Fake Field', tracker_pb2.FieldTypes.USER_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+
+ self.services.user.TestAddUser('owner@example.com', 111)
+ self.mr.project.owner_ids.extend([111])
+ owner = tracker_bizobj.MakeFieldValue(
+ fd.field_id, None, None, 111, None, None, False)
+
+ self.services.user.TestAddUser('committer@example.com', 222)
+ self.mr.project.committer_ids.extend([222])
+ self.mr.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=222,
+ perms=['FooPerm'])]
+ committer = tracker_bizobj.MakeFieldValue(
+ fd.field_id, None, None, 222, None, None, False)
+
+ self.services.user.TestAddUser('user@example.com', 333)
+ user = tracker_bizobj.MakeFieldValue(
+ fd.field_id, None, None, 333, None, None, False)
+
+ # Normal
+ for fv in (owner, committer, user):
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, fv)
+ self.assertIsNone(msg)
+
+ # Needs to be member (user isn't a member).
+ fd.needs_member = True
+ for fv in (owner, committer):
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, fv)
+ self.assertIsNone(msg)
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, user)
+ self.assertEqual('User must be a member of the project.', msg)
+
+ # Needs DeleteAny permission (only owner has it).
+ fd.needs_perm = 'DeleteAny'
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, owner)
+ self.assertIsNone(msg)
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, committer)
+ self.assertEqual('User must have permission "DeleteAny".', msg)
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, user)
+ self.assertEqual('User must be a member of the project.', msg)
+
+ # Needs custom permission (only committer has it).
+ fd.needs_perm = 'FooPerm'
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, owner)
+ self.assertEqual('User must have permission "FooPerm".', msg)
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, committer)
+ self.assertIsNone(msg)
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, user)
+ self.assertEqual('User must be a member of the project.', msg)
+
+ def test_DateType(self):
+ pass # TODO(jrobbins): write this test. @@@
+
+ def test_UrlType(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.URL_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+
+ fv = tracker_bizobj.MakeFieldValue(
+ 123, None, None, None, None, 'www.google.com', False)
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, fv)
+ self.assertIsNone(msg)
+
+ fv.url_value = 'go/puppies'
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, fv)
+ self.assertIsNone(msg)
+
+ fv.url_value = 'go/213'
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, fv)
+ self.assertIsNone(msg)
+
+ fv.url_value = 'puppies'
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, fv)
+ self.assertEqual('Value must be a valid url.', msg)
+
+ def test_OtherType(self):
+ # There are currently no validation options for date-type custom fields.
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'Deadline', tracker_pb2.FieldTypes.DATE_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ fv = tracker_bizobj.MakeFieldValue(
+ 123, None, None, None, 1234567890, None, False)
+ msg = field_helpers.ValidateCustomFieldValue(
+ self.mr.cnxn, self.mr.project, self.services, fd, fv)
+ self.assertIsNone(msg)
+
+ def testValidateCustomFields_NoCustomFieldValues(self):
+ err_msgs = field_helpers.ValidateCustomFields(
+ self.mr, self.services, [], self.config, self.mr.project,
+ ezt_errors=self.errors)
+ self.assertFalse(self.errors.AnyErrors())
+ self.assertEqual(err_msgs, [])
+
+ def testValidateCustomFields_NoErrors(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.config.field_defs.append(fd)
+ fv1 = tracker_bizobj.MakeFieldValue(
+ 123, 8086, None, None, None, None, False)
+ fv2 = tracker_bizobj.MakeFieldValue(123, 486, None, None, None, None, False)
+
+ err_msgs = field_helpers.ValidateCustomFields(
+ self.mr, self.services, [fv1, fv2], self.config, self.mr.project,
+ ezt_errors=self.errors)
+ self.assertFalse(self.errors.AnyErrors())
+ self.assertEqual(err_msgs, [])
+
+ def testValidateCustomFields_SomeValueErrors(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.config.field_defs.append(fd)
+ fv1 = tracker_bizobj.MakeFieldValue(
+ 123, 8086, None, None, None, None, False)
+ fv2 = tracker_bizobj.MakeFieldValue(123, 486, None, None, None, None, False)
+
+ fd.min_value = 1
+ fd.max_value = 999
+ err_msgs = field_helpers.ValidateCustomFields(
+ self.mr, self.services, [fv1, fv2], self.config, self.mr.project,
+ ezt_errors=self.errors)
+ self.assertTrue(self.errors.AnyErrors())
+ self.assertEqual(1, len(self.errors.custom_fields))
+ custom_field_error = self.errors.custom_fields[0]
+ self.assertEqual(123, custom_field_error.field_id)
+ self.assertEqual('Value must be <= 999.', custom_field_error.message)
+ self.assertEqual(len(err_msgs), 1)
+ self.assertTrue(re.search(r'Value must be <= 999.', err_msgs[0]))
+
+ def testValidateCustomFields_DeletedRequiredFields_Ignored(self):
+ issue = fake.MakeTestIssue(
+ 789,
+ 1,
+ 'sum',
+ 'New',
+ 111,
+ issue_id=78901,
+ labels=['type-defect', 'other-label'])
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None, '', False,
+ False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', True)
+ self.config.field_defs.append(fd)
+
+ err_msgs = field_helpers.ValidateCustomFields(
+ self.mr,
+ self.services, [],
+ self.config,
+ self.mr.project,
+ ezt_errors=self.errors,
+ issue=issue)
+ self.assertFalse(self.errors.AnyErrors())
+ self.assertEqual(err_msgs, [])
+
+ def testValidateCustomFields_RequiredFields_Normal(self):
+ issue = fake.MakeTestIssue(
+ 789,
+ 1,
+ 'sum',
+ 'New',
+ 111,
+ issue_id=78901,
+ labels=['type-defect', 'other-label'])
+
+ required = True
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None, '', required,
+ False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.config.field_defs.append(fd)
+ fv1 = tracker_bizobj.MakeFieldValue(
+ 123, 8086, None, None, None, None, False)
+ fv2 = tracker_bizobj.MakeFieldValue(123, 486, None, None, None, None, False)
+
+ err_msgs = field_helpers.ValidateCustomFields(
+ self.mr,
+ self.services, [fv1, fv2],
+ self.config,
+ self.mr.project,
+ issue=issue)
+ self.assertEqual(len(err_msgs), 0)
+
+ def testValidateCustomFields_RequiredFields_ErrorsWhenMissing(self):
+ issue = fake.MakeTestIssue(
+ 789,
+ 1,
+ 'sum',
+ 'New',
+ 111,
+ issue_id=78901,
+ labels=['type-defect', 'other-label'])
+
+ required = True
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None, '', required,
+ False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.config.field_defs.append(fd)
+
+ err_msgs = field_helpers.ValidateCustomFields(
+ self.mr, self.services, [], self.config, self.mr.project, issue=issue)
+ self.assertEqual(len(err_msgs), 1)
+ self.assertTrue(re.search(r'CPU field is required.', err_msgs[0]))
+
+ def testValidateCustomFields_RequiredFields_EnumFieldNormal(self):
+ # Enums are a special case because their values are stored in labels.
+ issue = fake.MakeTestIssue(
+ 789,
+ 1,
+ 'sum',
+ 'New',
+ 111,
+ issue_id=78901,
+ labels=['type-defect', 'other-label', 'CPU-enum-value'])
+ required = True
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.ENUM_TYPE, None, '', required,
+ False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.config.field_defs.append(fd)
+
+ err_msgs = field_helpers.ValidateCustomFields(
+ self.mr, self.services, [], self.config, self.mr.project, issue=issue)
+ self.assertEqual(len(err_msgs), 0)
+
+ def testValidateCustomFields_RequiredFields_EnumFieldMultiWord(self):
+ # Enum fields with dashes in them require special label prefix parsing.
+ issue = fake.MakeTestIssue(
+ 789,
+ 1,
+ 'sum',
+ 'New',
+ 111,
+ issue_id=78901,
+ labels=['type-defect', 'other-label', 'an-enum-value'])
+ required = True
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'an-enum', tracker_pb2.FieldTypes.ENUM_TYPE, None, '',
+ required, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.config.field_defs.append(fd)
+
+ err_msgs = field_helpers.ValidateCustomFields(
+ self.mr, self.services, [], self.config, self.mr.project, issue=issue)
+ self.assertEqual(len(err_msgs), 0)
+
+ def testValidateCustomFields_RequiredFields_EnumFieldError(self):
+ # Enums are a special case because their values are stored in labels.
+ issue = fake.MakeTestIssue(
+ 789,
+ 1,
+ 'sum',
+ 'New',
+ 111,
+ issue_id=78901,
+ labels=['type-defect', 'other-label'])
+ required = True
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.ENUM_TYPE, None, '', required,
+ False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.config.field_defs.append(fd)
+
+ err_msgs = field_helpers.ValidateCustomFields(
+ self.mr, self.services, [], self.config, self.mr.project, issue=issue)
+ self.assertEqual(len(err_msgs), 1)
+ self.assertTrue(re.search(r'CPU field is required.', err_msgs[0]))
+
+ def testAssertCustomFieldsEditPerms_Empty(self):
+ self.assertIsNone(
+ field_helpers.AssertCustomFieldsEditPerms(
+ self.mr, self.config, [], [], [], [], []))
+
+ def testAssertCustomFieldsEditPerms_Normal(self):
+ self.services.user.TestAddUser('user@example.com', 222)
+ self.mr.auth.effective_ids = {222}
+ fd_int = tracker_bizobj.MakeFieldDef(
+ 11111,
+ 1,
+ 'fdInt',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=True)
+ fd_str = tracker_bizobj.MakeFieldDef(
+ 22222,
+ 1,
+ 'fdStr',
+ tracker_pb2.FieldTypes.STR_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=True)
+ fd_date = tracker_bizobj.MakeFieldDef(
+ 33333,
+ 1,
+ 'fdDate',
+ tracker_pb2.FieldTypes.DATE_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=True)
+ fd_enum1 = tracker_bizobj.MakeFieldDef(
+ 44444,
+ 1,
+ 'fdEnum1',
+ tracker_pb2.FieldTypes.ENUM_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=True)
+ fd_enum2 = tracker_bizobj.MakeFieldDef(
+ 55555,
+ 1,
+ 'fdEnum2',
+ tracker_pb2.FieldTypes.ENUM_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=True)
+ self.config.field_defs = [fd_int, fd_str, fd_date, fd_enum1, fd_enum2]
+ fv1 = tracker_bizobj.MakeFieldValue(
+ 11111, 37, None, None, None, None, False)
+ fv2 = tracker_bizobj.MakeFieldValue(
+ 22222, None, 'Chicken', None, None, None, False)
+ self.assertIsNone(
+ field_helpers.AssertCustomFieldsEditPerms(
+ self.mr, self.config, [fv1], [fv2], [33333], ['Dog', 'fdEnum1-a'],
+ ['Cat', 'fdEnum2-b']))
+
+ def testAssertCustomFieldsEditPerms_Reject(self):
+ self.mr.perms = permissions.PermissionSet([])
+ fd_int = tracker_bizobj.MakeFieldDef(
+ 11111,
+ 1,
+ 'fdInt',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=True)
+ fd_enum = tracker_bizobj.MakeFieldDef(
+ 44444,
+ 1,
+ 'fdEnum',
+ tracker_pb2.FieldTypes.ENUM_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=True)
+ self.config.field_defs = [fd_int, fd_enum]
+ fv = tracker_bizobj.MakeFieldValue(11111, 37, None, None, None, None, False)
+
+ self.assertRaises(
+ AssertionError, field_helpers.AssertCustomFieldsEditPerms, self.mr,
+ self.config, [fv], [], [], [], [])
+
+ self.assertRaises(
+ AssertionError, field_helpers.AssertCustomFieldsEditPerms, self.mr,
+ self.config, [], [fv], [], [], [])
+
+ self.assertRaises(
+ AssertionError, field_helpers.AssertCustomFieldsEditPerms, self.mr,
+ self.config, [], [], [11111], [], [])
+
+ self.assertRaises(
+ AssertionError, field_helpers.AssertCustomFieldsEditPerms, self.mr,
+ self.config, [], [], [], ['Dog', 'fdEnum-a'], [])
+
+ self.assertRaises(
+ AssertionError, field_helpers.AssertCustomFieldsEditPerms, self.mr,
+ self.config, [], [], [], [], ['Cat', 'fdEnum-b'])
+
+ def testApplyRestrictedDefaultValues(self):
+ self.mr.perms = permissions.PermissionSet([])
+ self.services.user.TestAddUser('user@example.com', 222)
+ self.mr.auth.effective_ids = {222}
+ fd_int = tracker_bizobj.MakeFieldDef(
+ 11111,
+ 1,
+ 'fdInt',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=False)
+ fd_str = tracker_bizobj.MakeFieldDef(
+ 22222,
+ 1,
+ 'fdStr',
+ tracker_pb2.FieldTypes.STR_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=True)
+ fd_str_2 = tracker_bizobj.MakeFieldDef(
+ 33333,
+ 1,
+ 'fdStr_2',
+ tracker_pb2.FieldTypes.STR_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=False)
+ fd_enum = tracker_bizobj.MakeFieldDef(
+ 44444,
+ 1,
+ 'fdEnum',
+ tracker_pb2.FieldTypes.ENUM_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=False)
+ fd_restricted_enum = tracker_bizobj.MakeFieldDef(
+ 55555,
+ 1,
+ 'fdRestrictedEnum',
+ tracker_pb2.FieldTypes.ENUM_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=True)
+ self.config.field_defs = [
+ fd_int, fd_str, fd_str_2, fd_enum, fd_restricted_enum
+ ]
+ fv = tracker_bizobj.MakeFieldValue(
+ 33333, None, 'Happy', None, None, None, False)
+ temp_fv = tracker_bizobj.MakeFieldValue(
+ 11111, 37, None, None, None, None, False)
+ temp_restricted_fv = tracker_bizobj.MakeFieldValue(
+ 22222, None, 'Chicken', None, None, None, False)
+ field_vals = [fv]
+ labels = ['Car', 'Bus']
+ temp_field_vals = [temp_fv, temp_restricted_fv]
+ temp_labels = ['Bike', 'fdEnum-a', 'fdRestrictedEnum-b']
+ field_helpers.ApplyRestrictedDefaultValues(
+ self.mr, self.config, field_vals, labels, temp_field_vals, temp_labels)
+ self.assertEqual(labels, ['Car', 'Bus', 'fdRestrictedEnum-b'])
+ self.assertEqual(field_vals, [fv, temp_restricted_fv])
+
+ def testFormatUrlFieldValue(self):
+ self.assertEqual('http://www.google.com',
+ field_helpers.FormatUrlFieldValue('www.google.com'))
+ self.assertEqual('https://www.bing.com',
+ field_helpers.FormatUrlFieldValue('https://www.bing.com'))
+
+ def testReviseFieldDefFromParsed_INT(self):
+ parsed_field_def = field_helpers.ParsedFieldDef(
+ 'EstDays',
+ 'int_type',
+ min_value=5,
+ max_value=7,
+ regex='',
+ needs_member=True,
+ needs_perm='Commit',
+ grants_perm='View',
+ notify_on=tracker_pb2.NotifyTriggers.ANY_COMMENT,
+ is_required=True,
+ is_niche=True,
+ importance='required',
+ is_multivalued=True,
+ field_docstring='updated doc',
+ choices_text='',
+ applicable_type='Launch',
+ applicable_predicate='',
+ revised_labels=[],
+ date_action_str='ping_participants',
+ approvers_str='',
+ survey='',
+ parent_approval_name='',
+ is_phase_field=False,
+ is_restricted_field=False)
+
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'EstDays', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, 4, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False,
+ approval_id=3)
+
+ new_fd = field_helpers.ReviseFieldDefFromParsed(parsed_field_def, fd)
+ # assert INT fields
+ self.assertEqual(new_fd.min_value, 5)
+ self.assertEqual(new_fd.max_value, 7)
+
+ # assert USER fields
+ self.assertEqual(new_fd.notify_on, tracker_pb2.NotifyTriggers.ANY_COMMENT)
+ self.assertTrue(new_fd.needs_member)
+ self.assertEqual(new_fd.needs_perm, 'Commit')
+ self.assertEqual(new_fd.grants_perm, 'View')
+
+ # assert DATE fields
+ self.assertEqual(new_fd.date_action,
+ tracker_pb2.DateAction.PING_PARTICIPANTS)
+
+ # assert general fields
+ self.assertTrue(new_fd.is_required)
+ self.assertTrue(new_fd.is_niche)
+ self.assertEqual(new_fd.applicable_type, 'Launch')
+ self.assertEqual(new_fd.docstring, 'updated doc')
+ self.assertTrue(new_fd.is_multivalued)
+ self.assertEqual(new_fd.approval_id, 3)
+ self.assertFalse(new_fd.is_phase_field)
+ self.assertFalse(new_fd.is_restricted_field)
+
+ def testParsedFieldDefAssertions_Accepted(self):
+ parsed_fd = field_helpers.ParsedFieldDef(
+ 'EstDays',
+ 'int_type',
+ min_value=5,
+ max_value=7,
+ regex='',
+ needs_member=True,
+ needs_perm='Commit',
+ grants_perm='View',
+ notify_on=tracker_pb2.NotifyTriggers.ANY_COMMENT,
+ is_required=True,
+ is_niche=False,
+ importance='required',
+ is_multivalued=True,
+ field_docstring='updated doc',
+ choices_text='',
+ applicable_type='Launch',
+ applicable_predicate='',
+ revised_labels=[],
+ date_action_str='ping_participants',
+ approvers_str='',
+ survey='',
+ parent_approval_name='',
+ is_phase_field=False,
+ is_restricted_field=False)
+
+ field_helpers.ParsedFieldDefAssertions(self.mr, parsed_fd)
+ self.assertFalse(self.mr.errors.AnyErrors())
+
+ def testParsedFieldDefAssertions_Rejected(self):
+ parsed_fd = field_helpers.ParsedFieldDef(
+ 'restrictApprovalField',
+ 'approval_type',
+ min_value=10,
+ max_value=7,
+ regex='/foo(?)/',
+ needs_member=True,
+ needs_perm='Commit',
+ grants_perm='View',
+ notify_on=tracker_pb2.NotifyTriggers.ANY_COMMENT,
+ is_required=True,
+ is_niche=True,
+ importance='required',
+ is_multivalued=True,
+ field_docstring='updated doc',
+ choices_text='',
+ applicable_type='Launch',
+ applicable_predicate='',
+ revised_labels=[],
+ date_action_str='custom_date_action_str',
+ approvers_str='',
+ survey='',
+ parent_approval_name='',
+ is_phase_field=False,
+ is_restricted_field=True)
+
+ field_helpers.ParsedFieldDefAssertions(self.mr, parsed_fd)
+ self.assertTrue(self.mr.errors.AnyErrors())
+
+ self.assertEqual(
+ self.mr.errors.is_niche, 'A field cannot be both required and niche.')
+ self.assertEqual(
+ self.mr.errors.date_action,
+ 'The date action should be either: ' + ', '.join(
+ config_svc.DATE_ACTION_ENUM) + '.')
+ self.assertEqual(
+ self.mr.errors.min_value, 'Minimum value must be less than maximum.')
+ self.assertEqual(self.mr.errors.regex, 'Invalid regular expression.')
diff --git a/tracker/test/fieldcreate_test.py b/tracker/test/fieldcreate_test.py
new file mode 100644
index 0000000..580d095
--- /dev/null
+++ b/tracker/test/fieldcreate_test.py
@@ -0,0 +1,301 @@
+# Copyright 2016 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
+
+"""Unit tests for the fieldcreate servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mox
+import mock
+import unittest
+import logging
+
+import ezt
+
+from framework import permissions
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import fieldcreate
+from tracker import tracker_bizobj
+
+
+class FieldCreateTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ config=fake.ConfigService(),
+ project=fake.ProjectService())
+ self.servlet = fieldcreate.FieldCreate(
+ 'req', 'res', services=self.services)
+ self.project = self.services.project.TestAddProject('proj')
+ self.mr = testing_helpers.MakeMonorailRequest(
+ project=self.project, perms=permissions.OWNER_ACTIVE_PERMISSIONSET)
+ self.services.user.TestAddUser('gatsby@example.com', 111)
+ self.services.user.TestAddUser('sport@example.com', 222)
+
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testAssertBasePermission(self):
+ # Anon users can never do it
+ self.mr.perms = permissions.READ_ONLY_PERMISSIONSET
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, self.mr)
+
+ # Project owner can do it.
+ self.mr.perms = permissions.OWNER_ACTIVE_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+
+ # Project member cannot do it
+ self.mr.perms = permissions.COMMITTER_ACTIVE_PERMISSIONSET
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, self.mr)
+ self.mr.perms = permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, self.mr)
+
+ def testGatherPageData(self):
+ approval_fd = tracker_bizobj.MakeFieldDef(
+ 1, self.mr.project_id, 'LaunchApproval',
+ tracker_pb2.FieldTypes.APPROVAL_TYPE, None, '', False,
+ False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'some approval thing', False)
+ config = self.services.config.GetProjectConfig(
+ self.mr.cnxn, self.mr.project_id)
+ config.field_defs.append(approval_fd)
+ self.services.config.StoreConfig(self.cnxn, config)
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual(self.servlet.PROCESS_TAB_LABELS,
+ page_data['admin_tab_mode'])
+ self.assertItemsEqual(
+ ['Defect', 'Enhancement', 'Task', 'Other'],
+ page_data['well_known_issue_types'])
+ self.assertEqual(['LaunchApproval'], page_data['approval_names'])
+ self.assertEqual('', page_data['initial_admins'])
+ self.assertEqual('', page_data['initial_editors'])
+ self.assertIsNone(page_data['initial_is_restricted_field'])
+
+ def testProcessFormData(self):
+ post_data = fake.PostData(
+ name=['somefield'],
+ field_type=['INT_TYPE'],
+ min_value=['1'],
+ max_value=['99'],
+ notify_on=['any_comment'],
+ importance=['required'],
+ is_multivalued=['Yes'],
+ docstring=['It is just some field'],
+ applicable_type=['Defect'],
+ date_action=['no_action'],
+ admin_names=['gatsby@example.com'],
+ editor_names=['sport@example.com'],
+ is_restricted_field=['Yes'])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertTrue('/adminLabels?saved=1&' in url)
+ config = self.services.config.GetProjectConfig(
+ self.mr.cnxn, self.mr.project_id)
+
+ fd = tracker_bizobj.FindFieldDef('somefield', config)
+ self.assertEqual('somefield', fd.field_name)
+ self.assertEqual(tracker_pb2.FieldTypes.INT_TYPE, fd.field_type)
+ self.assertEqual(1, fd.min_value)
+ self.assertEqual(99, fd.max_value)
+ self.assertEqual(tracker_pb2.NotifyTriggers.ANY_COMMENT, fd.notify_on)
+ self.assertTrue(fd.is_required)
+ self.assertFalse(fd.is_niche)
+ self.assertTrue(fd.is_multivalued)
+ self.assertEqual('It is just some field', fd.docstring)
+ self.assertEqual('Defect', fd.applicable_type)
+ self.assertEqual('', fd.applicable_predicate)
+ self.assertEqual([111], fd.admin_ids)
+ self.assertEqual([222], fd.editor_ids)
+
+ def testProcessFormData_Reject_EditorsForNonRestrictedField(self):
+ # This method tests that an exception is raised
+ # when trying to add editors to a non restricted field.
+ post_data = fake.PostData(
+ name=['somefield'],
+ field_type=['INT_TYPE'],
+ min_value=['1'],
+ max_value=['99'],
+ notify_on=['any_comment'],
+ importance=['required'],
+ is_multivalued=['Yes'],
+ docstring=['It is just some field'],
+ applicable_type=['Defect'],
+ date_action=['no_action'],
+ admin_names=['gatsby@example.com'],
+ editor_names=['sport@example.com'])
+
+ self.assertRaises(
+ AssertionError, self.servlet.ProcessFormData, self.mr, post_data)
+
+ def testProcessFormData_Reject_EditorsForApprovalField(self):
+ #This method tests that an exception is raised
+ #when trying to add editors to an approval field.
+ post_data = fake.PostData(
+ name=['approval_field'],
+ field_type=['approval_type'],
+ notify_on=['any_comment'],
+ importance=['required'],
+ is_multivalued=['Yes'],
+ docstring=['It is just some field'],
+ applicable_type=['Defect'],
+ date_action=['no_action'],
+ approver_names=['gatsby@example.com'],
+ is_restricted_field=['Yes'],
+ admin_names=['gatsby@example.com'],
+ editor_names=[''])
+
+ self.assertRaises(
+ AssertionError, self.servlet.ProcessFormData, self.mr, post_data)
+
+ @mock.patch('framework.servlet.Servlet.PleaseCorrect')
+ def testProcessFormData_RejectAssertions(self, fake_servlet_pc):
+ #This method tests when errors are found using when the
+ #field_helpers.ParsedFieldDefAssertions is triggered.
+ post_data = fake.PostData(
+ name=['somefield'],
+ field_type=['INT_TYPE'],
+ min_value=['1'],
+ max_value=['99'],
+ notify_on=['any_comment'],
+ importance=['required'],
+ is_multivalued=['Yes'],
+ docstring=['It is just some field'],
+ applicable_type=['Defect'],
+ date_action=['wrong_date_action'],
+ admin_names=['gatsby@example.com'],
+ editor_names=[''])
+
+ self.servlet.ProcessFormData(self.mr, post_data)
+
+ fake_servlet_pc.assert_called_once_with(
+ self.mr,
+ initial_field_name='somefield',
+ initial_type='INT_TYPE',
+ initial_parent_approval_name='',
+ initial_field_docstring='It is just some field',
+ initial_applicable_type='Defect',
+ initial_applicable_predicate='',
+ initial_needs_member=None,
+ initial_needs_perm='',
+ initial_importance='required',
+ initial_is_multivalued='yes',
+ initial_grants_perm='',
+ initial_notify_on=1,
+ initial_date_action='wrong_date_action',
+ initial_choices='',
+ initial_approvers='',
+ initial_survey='',
+ initial_is_phase_field=False,
+ initial_admins='gatsby@example.com',
+ initial_editors='',
+ initial_is_restricted_field=False)
+ self.assertTrue(self.mr.errors.AnyErrors())
+
+
+ def testProcessFormData_RejectNoApprover(self):
+ post_data = fake.PostData(
+ name=['approvalField'],
+ field_type=['approval_type'],
+ approver_names=[''],
+ admin_names=[''],
+ editor_names=[''],
+ parent_approval_name=['UIApproval'],
+ is_phase_field=['on'])
+
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ self.mr,
+ initial_field_name=post_data.get('name'),
+ initial_type=post_data.get('field_type'),
+ initial_field_docstring=post_data.get('docstring', ''),
+ initial_applicable_type=post_data.get('applical_type', ''),
+ initial_applicable_predicate='',
+ initial_needs_member=ezt.boolean('needs_member' in post_data),
+ initial_needs_perm=post_data.get('needs_perm', '').strip(),
+ initial_importance=post_data.get('importance'),
+ initial_is_multivalued=ezt.boolean('is_multivalued' in post_data),
+ initial_grants_perm=post_data.get('grants_perm', '').strip(),
+ initial_notify_on=0,
+ initial_date_action=post_data.get('date_action'),
+ initial_choices=post_data.get('choices', ''),
+ initial_approvers=post_data.get('approver_names'),
+ initial_parent_approval_name=post_data.get('parent_approval_name', ''),
+ initial_survey=post_data.get('survey', ''),
+ initial_is_phase_field=False,
+ initial_admins=post_data.get('admin_names'),
+ initial_editors=post_data.get('editor_names'),
+ initial_is_restricted_field=False)
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual(
+ 'Please provide at least one default approver.',
+ self.mr.errors.approvers)
+ self.assertIsNone(url)
+
+
+class FieldCreateMethodsTest(unittest.TestCase):
+
+ def setUp(self):
+ self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+
+ def testFieldNameErrorMessage_NoConflict(self):
+ self.assertIsNone(fieldcreate.FieldNameErrorMessage(
+ 'somefield', self.config))
+
+ def testFieldNameErrorMessage_PrefixReserved(self):
+ self.assertEqual(
+ 'That name is reserved.',
+ fieldcreate.FieldNameErrorMessage('owner', self.config))
+
+ def testFieldNameErrorMessage_SuffixReserved(self):
+ self.assertEqual(
+ 'That suffix is reserved.',
+ fieldcreate.FieldNameErrorMessage('doh-approver', self.config))
+
+ def testFieldNameErrorMessage_AlreadyInUse(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.config.field_defs.append(fd)
+ self.assertEqual(
+ 'That name is already in use.',
+ fieldcreate.FieldNameErrorMessage('CPU', self.config))
+
+ def testFieldNameErrorMessage_PrefixOfExisting(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'sign-off', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.config.field_defs.append(fd)
+ self.assertEqual(
+ 'That name is a prefix of an existing field name.',
+ fieldcreate.FieldNameErrorMessage('sign', self.config))
+
+ def testFieldNameErrorMessage_IncludesExisting(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'opt', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.config.field_defs.append(fd)
+ self.assertEqual(
+ 'An existing field name is a prefix of that name.',
+ fieldcreate.FieldNameErrorMessage('opt-in', self.config))
diff --git a/tracker/test/fielddetail_test.py b/tracker/test/fielddetail_test.py
new file mode 100644
index 0000000..f9f27b4
--- /dev/null
+++ b/tracker/test/fielddetail_test.py
@@ -0,0 +1,355 @@
+# Copyright 2016 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
+
+"""Unit tests for the fielddetail servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mox
+import unittest
+import logging
+
+import webapp2
+
+import ezt
+
+from framework import permissions
+from proto import project_pb2
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import fielddetail
+from tracker import tracker_bizobj
+from tracker import tracker_views
+
+
+class FieldDetailTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ config=fake.ConfigService(),
+ project=fake.ProjectService())
+ self.servlet = fielddetail.FieldDetail(
+ 'req', 'res', services=self.services)
+ self.project = self.services.project.TestAddProject('proj')
+ self.mr = testing_helpers.MakeMonorailRequest(
+ project=self.project, perms=permissions.OWNER_ACTIVE_PERMISSIONSET)
+ self.config = self.services.config.GetProjectConfig(
+ 'fake cnxn', self.project.project_id)
+ self.services.config.StoreConfig('fake cnxn', self.config)
+ self.fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.fd.admin_ids = [111]
+ self.fd.editor_ids = [222]
+ self.config.field_defs.append(self.fd)
+ self.services.user.TestAddUser('gatsby@example.com', 111)
+ self.services.user.TestAddUser('sport@example.com', 222)
+ self.mr.field_name = 'CPU'
+
+ # Approvals
+ self.approval_def = tracker_pb2.ApprovalDef(
+ approval_id=234, approver_ids=[111], survey='Question 1?')
+ self.sub_fd = tracker_pb2.FieldDef(
+ field_name='UIMocks', approval_id=234, applicable_type='')
+ self.sub_fd_deleted = tracker_pb2.FieldDef(
+ field_name='UIMocksDeleted', approval_id=234, applicable_type='',
+ is_deleted=True)
+ self.config.field_defs.extend([self.sub_fd, self.sub_fd_deleted])
+ self.config.approval_defs.append(self.approval_def)
+ self.approval_fd = tracker_bizobj.MakeFieldDef(
+ 234, 789, 'UIReview', tracker_pb2.FieldTypes.APPROVAL_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.config.field_defs.append(self.approval_fd)
+
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testGetFieldDef_NotFound(self):
+ self.mr.field_name = 'NeverHeardOfIt'
+ self.assertRaises(
+ webapp2.HTTPException,
+ self.servlet._GetFieldDef, self.mr)
+
+ def testGetFieldDef_Normal(self):
+ actual_config, actual_fd = self.servlet._GetFieldDef(self.mr)
+ self.assertEqual(self.config, actual_config)
+ self.assertEqual(self.fd, actual_fd)
+
+ def testAssertBasePermission_AnyoneCanView(self):
+ self.servlet.AssertBasePermission(self.mr)
+ self.mr.perms = permissions.COMMITTER_ACTIVE_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+ self.mr.perms = permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+ self.mr.perms = permissions.READ_ONLY_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+
+ def testAssertBasePermission_MembersOnly(self):
+ self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ # The project members can view the field definition.
+ self.servlet.AssertBasePermission(self.mr)
+ self.mr.perms = permissions.COMMITTER_ACTIVE_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+ self.mr.perms = permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+ # Non-member is not allowed to view anything in the project.
+ self.mr.perms = permissions.EMPTY_PERMISSIONSET
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, self.mr)
+
+ def testGatherPageData_ReadWrite(self):
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual(self.servlet.PROCESS_TAB_LABELS,
+ page_data['admin_tab_mode'])
+ self.assertTrue(page_data['allow_edit'])
+ self.assertEqual('gatsby@example.com', page_data['initial_admins'])
+ self.assertEqual('sport@example.com', page_data['initial_editors'])
+ field_def_view = page_data['field_def']
+ self.assertEqual('CPU', field_def_view.field_name)
+ self.assertEqual(page_data['approval_subfields'], [])
+ self.assertEqual(page_data['initial_approvers'], '')
+
+ def testGatherPageData_ReadOnly(self):
+ self.mr.perms = permissions.READ_ONLY_PERMISSIONSET
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual(self.servlet.PROCESS_TAB_LABELS,
+ page_data['admin_tab_mode'])
+ self.assertFalse(page_data['allow_edit'])
+ self.assertEqual('gatsby@example.com', page_data['initial_admins'])
+ self.assertEqual('sport@example.com', page_data['initial_editors'])
+ field_def_view = page_data['field_def']
+ self.assertEqual('CPU', field_def_view.field_name)
+ self.assertEqual(page_data['approval_subfields'], [])
+ self.assertEqual(page_data['initial_approvers'], '')
+
+ def testGatherPageData_Approval(self):
+ self.mr.field_name = 'UIReview'
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual(page_data['approval_subfields'], [self.sub_fd])
+ self.assertEqual(page_data['initial_approvers'], 'gatsby@example.com')
+ field_def_view = page_data['field_def']
+ self.assertEqual(field_def_view.field_name, 'UIReview')
+ self.assertEqual(field_def_view.survey, 'Question 1?')
+
+ def testProcessFormData_Permission(self):
+ """Only owners can edit fields."""
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET)
+ mr.field_name = 'CPU'
+ post_data = fake.PostData(
+ name=['CPU'],
+ deletefield=['Submit'])
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.ProcessFormData, mr, post_data)
+
+ self.servlet.ProcessFormData(self.mr, post_data)
+
+ def testProcessFormData_Delete(self):
+ post_data = fake.PostData(
+ name=['CPU'],
+ deletefield=['Submit'])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertTrue('/adminLabels?deleted=1&' in url)
+ fd = tracker_bizobj.FindFieldDef('CPU', self.config)
+ self.assertEqual('CPU', fd.field_name)
+ self.assertTrue(fd.is_deleted)
+
+ def testProcessFormData_Cancel(self):
+ post_data = fake.PostData(
+ name=['CPU'],
+ cancel=['Submit'],
+ max_value=['200'])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ logging.info(url)
+ self.assertTrue('/adminLabels?ts=' in url)
+ config = self.services.config.GetProjectConfig(
+ self.mr.cnxn, self.mr.project_id)
+
+ fd = tracker_bizobj.FindFieldDef('CPU', config)
+ self.assertIsNone(fd.max_value)
+ self.assertIsNone(fd.min_value)
+
+ def testProcessFormData_Edit(self):
+ post_data = fake.PostData(
+ name=['CPU'],
+ field_type=['INT_TYPE'],
+ min_value=['2'],
+ max_value=['98'],
+ notify_on=['never'],
+ is_required=[],
+ is_multivalued=[],
+ docstring=['It is just some field'],
+ applicable_type=['Defect'],
+ admin_names=['gatsby@example.com'])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertTrue('/fields/detail?field=CPU&saved=1&' in url)
+ config = self.services.config.GetProjectConfig(
+ self.mr.cnxn, self.mr.project_id)
+
+ fd = tracker_bizobj.FindFieldDef('CPU', config)
+ self.assertEqual('CPU', fd.field_name)
+ self.assertEqual(2, fd.min_value)
+ self.assertEqual(98, fd.max_value)
+ self.assertEqual([111], fd.admin_ids)
+ self.assertEqual([], fd.editor_ids)
+
+ def testProcessDeleteField(self):
+ self.servlet._ProcessDeleteField(self.mr, self.config, self.fd)
+ self.assertTrue(self.fd.is_deleted)
+
+ def testProcessDeleteField_subfields(self):
+ approval_fd = tracker_bizobj.MakeFieldDef(
+ 3, 789, 'Legal', tracker_pb2.FieldTypes.APPROVAL_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.fd.approval_id=3
+ self.config.field_defs.append(approval_fd)
+ self.servlet._ProcessDeleteField(self.mr, self.config, approval_fd)
+ self.assertTrue(self.fd.is_deleted)
+ self.assertTrue(approval_fd.is_deleted)
+
+ def testProcessEditField_Normal(self):
+ post_data = fake.PostData(
+ name=['CPU'],
+ field_type=['INT_TYPE'],
+ min_value=['2'],
+ admin_names=['gatsby@example.com'],
+ editor_names=['sport@example.com'],
+ is_restricted_field=['Yes'])
+ self.servlet._ProcessEditField(
+ self.mr, post_data, self.config, self.fd)
+ fd = tracker_bizobj.FindFieldDef('CPU', self.config)
+ self.assertEqual('CPU', fd.field_name)
+ self.assertEqual(2, fd.min_value)
+ self.assertEqual([111], fd.admin_ids)
+ self.assertEqual([222], fd.editor_ids)
+
+ def testProcessEditField_Reject(self):
+ post_data = fake.PostData(
+ name=['CPU'],
+ field_type=['INT_TYPE'],
+ min_value=['4'],
+ max_value=['1'],
+ admin_names=[''],
+ editor_names=[''])
+
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ self.mr,
+ field_def=mox.IgnoreArg(),
+ initial_applicable_type='',
+ initial_choices='',
+ initial_admins='',
+ initial_editors='',
+ initial_approvers='',
+ initial_is_restricted_field=False)
+ self.mox.ReplayAll()
+
+ url = self.servlet._ProcessEditField(
+ self.mr, post_data, self.config, self.fd)
+ self.assertEqual('Minimum value must be less than maximum.',
+ self.mr.errors.min_value)
+ self.assertIsNone(url)
+
+ fd = tracker_bizobj.FindFieldDef('CPU', self.config)
+ self.assertIsNone(fd.min_value)
+ self.assertIsNone(fd.max_value)
+
+ def testProcessEditField_Reject_EditorsForNonRestrictedField(self):
+ # This method tests that an exception is raised
+ # when trying to add editors to a non restricted field.
+ post_data = fake.PostData(
+ name=['CPU'],
+ field_type=['INT_TYPE'],
+ min_value=['2'],
+ admin_names=[''],
+ editor_names=['gatsby@example.com'])
+
+ self.assertRaises(
+ AssertionError, self.servlet._ProcessEditField, self.mr, post_data,
+ self.config, self.fd)
+
+ def testProcessEditField_RejectAssertions_1(self):
+ # This method tests that an exception is raised
+ # when trying to add editors to an approval field.
+ post_data = fake.PostData(
+ name=['CPU'],
+ approver_names=['gatsby@example.com'],
+ admin_names=[''],
+ editor_names=['sports@example.com'])
+
+ self.assertRaises(
+ AssertionError, self.servlet._ProcessEditField, self.mr, post_data,
+ self.config, self.approval_fd)
+
+ def testProcessEditField_RejectAssertions_2(self):
+ #This method tests that an exception is raised
+ #when trying to restrict an approval field.
+ post_data = fake.PostData(
+ name=['CPU'],
+ approver_names=['gatsby@example.com'],
+ is_restricted_field=['Yes'],
+ admin_names=[''],
+ editor_names=[''])
+
+ self.assertRaises(
+ AssertionError, self.servlet._ProcessEditField, self.mr, post_data,
+ self.config, self.approval_fd)
+
+ def testProcessEditField_RejectApproval(self):
+ self.mr.field_name = 'UIReview'
+ post_data = fake.PostData(
+ name=['UIReview'],
+ admin_names=[''],
+ editor_names=[''],
+ survey=['WIll there be UI changes?'],
+ approver_names=[''])
+
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ self.mr,
+ field_def=mox.IgnoreArg(),
+ initial_applicable_type='',
+ initial_choices='',
+ initial_admins='',
+ initial_editors='',
+ initial_approvers='',
+ initial_is_restricted_field=False)
+ self.mox.ReplayAll()
+
+ url = self.servlet._ProcessEditField(
+ self.mr, post_data, self.config, self.approval_fd)
+ self.assertEqual('Please provide at least one default approver.',
+ self.mr.errors.approvers)
+ self.assertIsNone(url)
+
+ def testProcessEditField_Approval(self):
+ self.mr.field_name = 'UIReview'
+ post_data = fake.PostData(
+ name=['UIReview'],
+ admin_names=[''],
+ editor_names=[''],
+ survey=['WIll there be UI changes?'],
+ approver_names=['sport@example.com, gatsby@example.com'])
+
+
+ url = self.servlet._ProcessEditField(
+ self.mr, post_data, self.config, self.approval_fd)
+ self.assertTrue('/fields/detail?field=UIReview&saved=1&' in url)
+
+ approval_def = tracker_bizobj.FindApprovalDef('UIReview', self.config)
+ self.assertEqual(len(approval_def.approver_ids), 2)
+ self.assertEqual(sorted(approval_def.approver_ids), sorted([111, 222]))
diff --git a/tracker/test/fltconversion_test.py b/tracker/test/fltconversion_test.py
new file mode 100644
index 0000000..e0bae41
--- /dev/null
+++ b/tracker/test/fltconversion_test.py
@@ -0,0 +1,930 @@
+# 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
+
+"""Unittests for the flt launch issues conversion task."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+import copy
+import unittest
+import settings
+import mock
+
+from businesslogic import work_env
+from framework import exceptions
+from framework import permissions
+from services import service_manager
+from services import template_svc
+from tracker import fltconversion
+from tracker import tracker_bizobj
+from testing import fake
+from testing import testing_helpers
+from proto import tracker_pb2
+
+class FLTConvertTask(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ template=mock.Mock(spec=template_svc.TemplateService),)
+ self.mr = testing_helpers.MakeMonorailRequest()
+ self.task = fltconversion.FLTConvertTask(
+ 'req', 'res', services=self.services)
+ self.task.mr = self.mr
+ self.issue = fake.MakeTestIssue(
+ 789, 1, 'summary', 'New', 111, issue_id=78901)
+ self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ self.work_env = work_env.WorkEnv(
+ self.mr, self.services, 'Testing')
+ self.issue1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, issue_id=78901,
+ labels=[
+ 'Launch-M-Approved-71-Stable', 'Launch-M-Target-70-Beta',
+ 'Launch-UI-Yes', 'Launch-Privacy-NeedInfo',
+ 'pm-jojwang', 'tl-annajo', 'ux-shiba', 'Type-Launch'])
+ self.issue2 = fake.MakeTestIssue(
+ 789, 2, 'sum', 'New', 111, issue_id=78902,
+ labels=[
+ 'Launch-M-Target-71-Stable', 'Launch-M-Approved-70-Beta',
+ 'pm-jojwang', 'tl-annajo', 'OS-Chrome', 'OS-Android',
+ 'Type-Launch'])
+ self.issue3 = fake.MakeTestIssue(
+ 789, 3, 'sum', 'New', 111, issue_id=78903,
+ labels=['Launch-M-Approved-71-Stable',
+ 'Launch-M-Approved-79-Stable-Exp', 'Launch-M-Target-70-Beta',
+ 'Launch-M-Target-70-Stable', 'Launch-UI-Yes',
+ 'Launch-Exp-Leadership-Yes', 'pm-annajo', 'tl-jojwang',
+ 'OS-Chrome', 'Type-Launch'])
+ self.issue4 = fake.MakeTestIssue(
+ 789, 4, 'sum', 'New', 111, issue_id=78904,
+ labels=['Launch-UI-Yes', 'OS-Chrome', 'Type-Launch'])
+ self.issue5 = fake.MakeTestIssue(
+ 789, 5, 'sum', 'New', 111, issue_id=78905,
+ labels=['Launch-M-Approved-71-Stable',
+ 'Launch-M-Approved-79-Stable-Exp', 'Launch-M-Target-70-Beta',
+ 'Launch-M-Target-70-Stable', 'Launch-UI-Yes',
+ 'Launch-Privacy-NeedInfo', 'Launch-Exp-Leadership-Yes',
+ 'pm-annajo', 'tl-jojwang', 'OS-Chrome', 'Type-Launch'])
+
+ self.approval_values = [
+ tracker_pb2.ApprovalValue(
+ approval_id=7, status=tracker_pb2.ApprovalStatus.NOT_SET),
+ tracker_pb2.ApprovalValue(
+ approval_id=8, status=tracker_pb2.ApprovalStatus.NEEDS_REVIEW)]
+ self.phases = [tracker_pb2.Phase(name='Stable', phase_id=88),
+ tracker_pb2.Phase(name='Beta', phase_id=89)]
+
+ def testAssertBasePermission(self):
+ self.mr.auth.user_pb.is_site_admin = True
+ settings.app_id = 'monorail-staging'
+ self.task.AssertBasePermission(self.mr)
+
+ settings.app_id = 'monorail-prod'
+ self.task.AssertBasePermission(self.mr)
+
+ self.mr.auth.user_pb.is_site_admin = False
+ self.assertRaises(permissions.PermissionException,
+ self.task.AssertBasePermission, self.mr)
+
+ def testHandleRequest(self):
+ # Set up Objects
+ project_info = fltconversion.ProjectInfo(
+ self.config, 'q=query', self.approval_values, self.phases,
+ 11, 12, 13, 16, 14, 15, fltconversion.BROWSER_PHASE_MAP,
+ fltconversion.BROWSER_APPROVALS_TO_LABELS,
+ fltconversion.BROWSER_M_LABELS_RE)
+
+ self.config.field_defs = [
+ tracker_pb2.FieldDef(field_id=7, field_name='Chrome-UX',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE),
+ tracker_pb2.FieldDef(field_id=8, field_name='Chrome-Privacy',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE)
+ ]
+
+ # Set up mocks
+ patcher = mock.patch(
+ 'search.frontendsearchpipeline.FrontendSearchPipeline',
+ spec=True, visible_results=[self.issue1, self.issue2])
+ mockPipeline = patcher.start()
+
+ self.task.services.issue.GetIssue = mock.Mock(
+ side_effect=[self.issue1, self.issue2])
+
+ self.task.FetchAndAssertProjectInfo = mock.Mock(return_value=project_info)
+
+ with self.work_env as we:
+ we.ListIssues = mock.Mock(return_value=mockPipeline)
+
+ def side_effect(_cnxn, email):
+ if email == 'jojwang@chromium.org':
+ return 111
+ if email == 'annajo@google.com':
+ return 222
+ if email == 'shiba@google.com':
+ return 333
+ raise exceptions.NoSuchUserException
+ self.task.services.user.LookupUserID = mock.Mock(side_effect=side_effect)
+
+ self.task.ExecuteIssueChanges = mock.Mock(return_value=[])
+
+ # Call
+ json = self.task.HandleRequest(self.mr)
+
+ # assert
+ self.assertEqual(json['converted_issues'], [1, 2])
+
+ new_approvals1 = [
+ tracker_pb2.ApprovalValue(
+ approval_id=7, status=tracker_pb2.ApprovalStatus.APPROVED),
+ tracker_pb2.ApprovalValue(
+ approval_id=8, status=tracker_pb2.ApprovalStatus.NEED_INFO)]
+ new_fvs1 = [
+ # M-Approved Stable
+ tracker_bizobj.MakeFieldValue(
+ 15, 71, None, None, None, None, False, phase_id=88),
+ # M-Target Beta
+ tracker_bizobj.MakeFieldValue(
+ 14, 70, None, None, None, None, False, phase_id=89),
+ # PM field
+ tracker_bizobj.MakeFieldValue(
+ 11, None, None, 111, None, None, False),
+ # TL field
+ tracker_bizobj.MakeFieldValue(
+ 12, None, None, 222, None, None, False),
+ # UX field
+ tracker_bizobj.MakeFieldValue(
+ 16, None, None, 333, None, None, False)
+ ]
+
+
+ new_approvals2 = [
+ tracker_pb2.ApprovalValue(
+ approval_id=7, status=tracker_pb2.ApprovalStatus.NOT_SET),
+ tracker_pb2.ApprovalValue(
+ approval_id=8, status=tracker_pb2.ApprovalStatus.NEEDS_REVIEW)
+ ]
+ new_fvs2 = [
+ tracker_bizobj.MakeFieldValue(
+ 14, 71, None, None, None, None, False, phase_id=88),
+ tracker_bizobj.MakeFieldValue(
+ 15, 70, None, None, None, None, False, phase_id=89),
+ # PM field
+ tracker_bizobj.MakeFieldValue(
+ 11, None, None, 111, None, None, False),
+ # TL field
+ tracker_bizobj.MakeFieldValue(
+ 12, None, None, 222, None, None, False)]
+
+ execute_calls = [
+ mock.call(
+ self.config, self.issue1, new_approvals1, self.phases, new_fvs1),
+ mock.call(
+ self.config, self.issue2, new_approvals2, self.phases, new_fvs2)]
+ self.task.ExecuteIssueChanges.assert_has_calls(execute_calls)
+
+ patcher.stop()
+
+ def testHandleRequest_UndoConversion(self):
+ # test Delete() is actually called
+ mr = testing_helpers.MakeMonorailRequest(path='url/url?launch=delete')
+ self.task.UndoConversion = mock.Mock(return_value={'deleteing': [1, 2]})
+ actualReturn = self.task.HandleRequest(mr)
+ self.assertEqual({'deleteing': [1, 2]}, actualReturn)
+
+ def testUndoConversion(self):
+ # Set up objects
+ self.issue1.field_values = [
+ # Test non phase and TL/PM/TE field_values are not deleted
+ tracker_bizobj.MakeFieldValue(
+ 17, None, 'strvalue', None, None, None, False)]
+ issue1 = copy.deepcopy(self.issue1)
+ issue2 = copy.deepcopy(self.issue2)
+ fvs = [
+ tracker_bizobj.MakeFieldValue(
+ 11, None, None, 222, None, None, False),
+ tracker_bizobj.MakeFieldValue(
+ 12, None, None, 111, None, None, False),
+ tracker_bizobj.MakeFieldValue(
+ 16, None, None, 111, None, None, False)]
+ self.config.field_defs = [
+ tracker_pb2.FieldDef(field_id=11, field_name='PM'),
+ tracker_pb2.FieldDef(field_id=12, field_name='TL'),
+ tracker_pb2.FieldDef(field_id=13, field_name='TE'),
+ tracker_pb2.FieldDef(field_id=16, field_name='UX')]
+ # Make element edits made during conversion that should be undone.
+ issue1.labels.extend(['Type-FLT-Launch', 'FLT-Conversion'])
+ issue1.labels.remove('Type-Launch')
+ issue2.labels.extend(['Type-FLT-Launch', 'FLT-Conversion'])
+ issue2.labels.remove('Type-Launch')
+ issue1.approval_values = self.approval_values
+ issue2.approval_values = self.approval_values
+ issue1.phases = self.phases
+ issue2.phases = self.phases
+ issue1.field_values.extend(fvs)
+
+ # Set up mocks
+ patcher = mock.patch(
+ 'search.frontendsearchpipeline.FrontendSearchPipeline',
+ spec=True, visible_results=[issue1, issue2]) # converted issues
+ mockPipeline = patcher.start()
+ self.task.services.project.GetProjectByName = mock.Mock()
+ self.task.services.config.GetProjectConfig = mock.Mock(
+ return_value=self.config)
+ self.task.services.issue.GetIssue = mock.Mock(
+ side_effect=[issue1, issue2])
+ self.task.services.issue._UpdateIssuesApprovals = mock.Mock()
+ self.task.services.issue.UpdateIssue = mock.Mock()
+
+ with self.work_env as we:
+ we.ListIssues = mock.Mock(return_value=mockPipeline)
+
+ json = self.task.UndoConversion(self.mr)
+ self.assertEqual(json['deleting'], [1, 2])
+ # assert convert issue1 is back to the pre-conversion state, self.issue1.
+ self.assertEqual(issue1, self.issue1)
+ self.assertEqual(issue2, self.issue2)
+
+ # assert UpdateIssue calls were made with pre-conversion state issues.
+ update_calls = [
+ mock.call(self.mr.cnxn, self.issue1),
+ mock.call(self.mr.cnxn, self.issue2)]
+ self.task.services.issue._UpdateIssuesApprovals.assert_has_calls(
+ update_calls)
+ self.task.services.issue.UpdateIssue.assert_has_calls(update_calls)
+ patcher.stop()
+
+ def testVerifyConversion(self):
+ # Set up objects
+ self.issue1.labels.extend(
+ # Launch-M-Target-70-Stable-Exp should be ignored
+ ['Rollout-Type-Default', 'Launch-M-Target-70-Stable-Exp'])
+ self.issue1.phases = [tracker_pb2.Phase(name='Beta', phase_id=1),
+ tracker_pb2.Phase(name='Stable', phase_id=2)]
+ self.issue1.approval_values = [
+ tracker_pb2.ApprovalValue(
+ approval_id=1, status=tracker_pb2.ApprovalStatus.NOT_SET),
+ tracker_pb2.ApprovalValue(
+ approval_id=2, status=tracker_pb2.ApprovalStatus.APPROVED),
+ tracker_pb2.ApprovalValue(
+ approval_id=3, status=tracker_pb2.ApprovalStatus.NEED_INFO),
+ ]
+ self.issue1.field_values = [
+ # problem = expected field for TL
+ tracker_bizobj.MakeFieldValue(4, None, None, 111, None, None, False),
+ tracker_pb2.FieldValue(field_id=7, int_value=70, phase_id=1),
+ tracker_pb2.FieldValue(field_id=8, int_value=71, phase_id=2),
+ ]
+
+ self.issue2.labels.extend(['Rollout-Type-Finch'])
+ self.issue2.phases = [tracker_pb2.Phase(name='Beta', phase_id=1),
+ tracker_pb2.Phase(name='Stable-Full', phase_id=2),
+ tracker_pb2.Phase(name='Stable-Exp', phase_id=3)]
+ self.issue2.approval_values = [
+ tracker_pb2.ApprovalValue(
+ approval_id=1, status=tracker_pb2.ApprovalStatus.NOT_SET),
+ tracker_pb2.ApprovalValue(
+ approval_id=2, status=tracker_pb2.ApprovalStatus.NOT_SET),
+ tracker_pb2.ApprovalValue(
+ # problem = approval Chrome-Privacy has status approved for None
+ approval_id=3, status=tracker_pb2.ApprovalStatus.APPROVED),
+ ]
+ self.issue2.field_values = [
+ # problem = no phase field for label 'Launch-M-Approved-70-Beta'
+ tracker_pb2.FieldValue(field_id=7, int_value=71, phase_id=2),
+ tracker_bizobj.MakeFieldValue(4, None, None, 111, None, None, False),
+ tracker_bizobj.MakeFieldValue(5, None, None, 111, None, None, False),
+ ]
+
+ self.issue3.labels.extend(['Rollout-Type-Default'])
+ self.issue3.phases = [tracker_pb2.Phase(name='Feature Freeze', phase_id=4),
+ tracker_pb2.Phase(name='Branch', phase_id=5),
+ tracker_pb2.Phase(name='Stable', phase_id=6)]
+ self.issue3.approval_values = [
+ tracker_pb2.ApprovalValue(
+ approval_id=9, status=tracker_pb2.ApprovalStatus.APPROVED),
+ tracker_pb2.ApprovalValue(
+ approval_id=10, status=tracker_pb2.ApprovalStatus.NEEDS_REVIEW)]
+ # problem = no phase field label Launch-M-Target-70-Stable
+ # problem = missing a field for TL
+ self.issue3.field_values = [
+ tracker_pb2.FieldValue(field_id=8, int_value=71, phase_id=6),
+ tracker_bizobj.MakeFieldValue(4, None, None, 111, None, None, False)
+ ]
+
+ self.issue4.labels.extend(['Rollout-Type-Default'])
+ # problem = incorrect phases for OS default launch
+ self.issue4.phases = [tracker_pb2.Phase(name='Branch', phase_id=5),
+ tracker_pb2.Phase(name='Stable-Exp', phase_id=7)]
+ # problem = approval ChromeOS-UX has status 'NEEDS_REVIEW'
+ # for label value Yes
+ self.issue4.approval_values = [
+ tracker_pb2.ApprovalValue(
+ approval_id=9, status=tracker_pb2.ApprovalStatus.NEEDS_REVIEW)]
+
+ self.issue5.labels.extend(['Rollout-Type-Finch'])
+ self.issue5.phases = [tracker_pb2.Phase(name='Branch', phase_id=5),
+ tracker_pb2.Phase(name='Feature Freeze', phase_id=4),
+ tracker_pb2.Phase(name='Stable-Exp', phase_id=7),
+ tracker_pb2.Phase(name='Stable-Full', phase_id=8)]
+ self.issue5.approval_values = [
+ tracker_pb2.ApprovalValue(
+ approval_id=9, status=tracker_pb2.ApprovalStatus.APPROVED),
+ # problem = approval ChromeOS-Privacy has status 'REVIEW_REQUESTED'
+ # for label value NeedInfo
+ tracker_pb2.ApprovalValue(
+ approval_id=11, status=tracker_pb2.ApprovalStatus.REVIEW_REQUESTED),
+ # problem = approval ChromeOS-Leadership-Exp has status 'NA' for label
+ # value Yes.
+ tracker_pb2.ApprovalValue(
+ approval_id=13, status=tracker_pb2.ApprovalStatus.NA)
+ ]
+
+ # problem = no phase field for label Launch-M-Approved-79-Stable-Exp
+ # problem = no phase field for label Launch-M-Target-70-Stable
+ self.issue5.field_values = [
+ tracker_pb2.FieldValue(field_id=8, int_value=71, phase_id=8),
+ tracker_bizobj.MakeFieldValue(4, None, None, 111, None, None, False),
+ tracker_bizobj.MakeFieldValue(5, None, None, 111, None, None, False)]
+
+ self.config.field_defs = [
+ tracker_pb2.FieldDef(field_id=1, field_name='Chrome-Test'),
+ tracker_pb2.FieldDef(field_id=2, field_name='Chrome-UX'),
+ tracker_pb2.FieldDef(field_id=3, field_name='Chrome-Privacy'),
+ tracker_pb2.FieldDef(field_id=4, field_name='PM'),
+ tracker_pb2.FieldDef(field_id=5, field_name='TL'),
+ tracker_pb2.FieldDef(field_id=6, field_name='TE'),
+ tracker_pb2.FieldDef(field_id=12, field_name='UX'),
+ tracker_pb2.FieldDef(field_id=7, field_name='M-Target'),
+ tracker_pb2.FieldDef(field_id=8, field_name='M-Approved'),
+ tracker_pb2.FieldDef(field_id=9, field_name='ChromeOS-UX'),
+ tracker_pb2.FieldDef(field_id=10, field_name='ChromeOS-Enterprise'),
+ tracker_pb2.FieldDef(field_id=11, field_name='ChromeOS-Privacy'),
+ tracker_pb2.FieldDef(field_id=13, field_name='ChromeOS-Leadership-Exp')
+ ]
+
+ # Set up mocks
+ patcher = mock.patch(
+ 'search.frontendsearchpipeline.FrontendSearchPipeline',
+ spec=True, allowed_results=[
+ self.issue1, self.issue2, self.issue3, self.issue4, self.issue5])
+ mockPipeline = patcher.start()
+ self.task.services.project.GetProjectByName = mock.Mock()
+ self.task.services.config.GetProjectConfig = mock.Mock(
+ return_value=self.config)
+ self.task.services.issue.GetIssue = mock.Mock(
+ side_effect=[
+ self.issue1, self.issue2, self.issue3, self.issue4, self.issue5])
+ self.task.services.user.LookupUserID = mock.Mock(return_value=111)
+ with self.work_env as we:
+ we.ListIssues = mock.Mock(return_value=mockPipeline)
+
+ # Assert
+ json = self.task.VerifyConversion(self.mr)
+ self.assertEqual(json['issues verified'],
+ ['issue 1', 'issue 2', 'issue 3', 'issue 4', 'issue 5'])
+ problems = json['problems found']
+ expected_problems = [
+ 'issue 1: missing a field for TL',
+ 'issue 1: missing a field for UX',
+ 'issue 2: approval Chrome-Privacy has status \'APPROVED\' for '
+ 'label value None',
+ 'issue 2: no phase field for label Launch-M-Approved-70-Beta',
+ 'issue 3: missing a field for TL',
+ 'issue 3: no phase field for label Launch-M-Target-70-Stable',
+ 'issue 4: incorrect phases for OS default launch.',
+ 'issue 4: approval ChromeOS-UX has status \'NEEDS_REVIEW\' for '
+ 'label value Yes',
+ 'issue 5: approval ChromeOS-Privacy has status \'REVIEW_REQUESTED\' '
+ 'for label value NeedInfo',
+ 'issue 5: approval ChromeOS-Leadership-Exp has status \'NA\' for label '
+ 'value Yes',
+ 'issue 5: no phase field for label Launch-M-Approved-79-Stable-Exp',
+ 'issue 5: no phase field for label Launch-M-Target-70-Stable',
+ ]
+ self.assertEqual(problems, expected_problems)
+ patcher.stop()
+
+ def testFetchAndAssertProjectInfo(self):
+
+ # test no 'launch' in request
+ self.assertRaisesRegexp(
+ AssertionError, r'bad launch type:',
+ self.task.FetchAndAssertProjectInfo, self.mr)
+
+ # test bad 'launch' in request
+ mr = testing_helpers.MakeMonorailRequest(path='url/url?launch=bad')
+ self.assertRaisesRegexp(
+ AssertionError, r'bad launch type: bad',
+ self.task.FetchAndAssertProjectInfo, mr)
+
+ self.task.services.project.GetProjectByName = mock.Mock()
+ self.task.services.config.GetProjectConfig = mock.Mock(
+ return_value=self.config)
+
+ mr = testing_helpers.MakeMonorailRequest(path='url/url?launch=default')
+ # test no template
+ self.task.services.template.GetTemplateByName = mock.Mock(
+ return_value=None)
+ self.assertRaisesRegexp(
+ AssertionError, r'not found in chromium project',
+ self.task.FetchAndAssertProjectInfo, mr)
+
+ # test template has no phases/approvals
+ template = tracker_bizobj.MakeIssueTemplate(
+ 'template', 'sum', 'New', 111, 'content', [], [], [], [])
+ self.task.services.template.GetTemplateByName = mock.Mock(
+ return_value=template)
+ self.assertRaisesRegexp(
+ AssertionError, 'no approvals or phases in',
+ self.task.FetchAndAssertProjectInfo, mr)
+
+ # test phases not recognized
+ template.phases = [tracker_pb2.Phase(name='WeirdPhase')]
+ template.approval_values = [tracker_pb2.ApprovalValue()]
+ self.assertRaisesRegexp(
+ AssertionError, 'one or more phases not recognized',
+ self.task.FetchAndAssertProjectInfo, mr)
+
+ template.phases = [tracker_pb2.Phase(name='Stable'),
+ tracker_pb2.Phase(name='Stable-Exp')]
+ template.approval_values = [
+ tracker_pb2.ApprovalValue(approval_id=1),
+ tracker_pb2.ApprovalValue(approval_id=2),
+ tracker_pb2.ApprovalValue(approval_id=3)]
+
+ # test approvals not recognized
+ self.assertRaisesRegexp(
+ AssertionError, 'one or more approvals not recognized',
+ self.task.FetchAndAssertProjectInfo, mr)
+
+ self.config.field_defs = [
+ tracker_pb2.FieldDef(field_id=1, field_name='ChromeOS-Enterprise',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE),
+ tracker_pb2.FieldDef(field_id=2, field_name='Chrome-UX',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE),
+ tracker_pb2.FieldDef(field_id=3, field_name='Chrome-Privacy',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE)
+ ]
+
+ # test approvals not in config's approval_defs
+ self.assertRaisesRegexp(
+ AssertionError, 'one or more approvals not in config.approval_defs',
+ self.task.FetchAndAssertProjectInfo, mr)
+
+ self.config.approval_defs = [
+ tracker_pb2.ApprovalDef(approval_id=1),
+ tracker_pb2.ApprovalDef(approval_id=2),
+ tracker_pb2.ApprovalDef(approval_id=3)]
+
+ # test no pm field exists in project
+ self.assertRaisesRegexp(
+ AssertionError, 'project has no FieldDef %s' % fltconversion.PM_FIELD,
+ self.task.FetchAndAssertProjectInfo, mr)
+
+ self.config.field_defs.extend([
+ tracker_pb2.FieldDef(field_id=4, field_name='PM',
+ field_type=tracker_pb2.FieldTypes.USER_TYPE),
+ tracker_pb2.FieldDef(field_id=5, field_name='TL',
+ field_type=tracker_pb2.FieldTypes.USER_TYPE),
+ tracker_pb2.FieldDef(field_id=9, field_name='UX',
+ field_type=tracker_pb2.FieldTypes.USER_TYPE),
+ tracker_pb2.FieldDef(field_id=6, field_name='TE')
+ ])
+
+ # test no USER_TYPE te field exists in project
+ self.assertRaisesRegexp(
+ AssertionError, 'project has no FieldDef %s' % fltconversion.TE_FIELD,
+ self.task.FetchAndAssertProjectInfo, mr)
+
+ self.config.field_defs[-1].field_type = tracker_pb2.FieldTypes.USER_TYPE
+ self.config.field_defs.extend([
+ tracker_pb2.FieldDef(
+ field_id=7, field_name='M-Target', is_phase_field=True),
+ tracker_pb2.FieldDef(
+ field_id=8, field_name='M-Approved', is_multivalued=True,
+ field_type=tracker_pb2.FieldTypes.INT_TYPE)
+ ])
+
+ # test no M-Target INT_TYPE multivalued Phase FieldDefs
+ self.assertRaisesRegexp(
+ AssertionError,
+ 'project has no FieldDef %s' % fltconversion.MTARGET_FIELD,
+ self.task.FetchAndAssertProjectInfo, mr)
+
+ self.config.field_defs[-2].field_type = tracker_pb2.FieldTypes.INT_TYPE
+ self.config.field_defs[-2].is_multivalued = True
+
+ # test no M-Approved INT_TYPE multivalued Phase FieldDefs
+ self.assertRaisesRegexp(
+ AssertionError,
+ 'project has no FieldDef %s' % fltconversion.MAPPROVED_FIELD,
+ self.task.FetchAndAssertProjectInfo, mr)
+
+ self.config.field_defs[-1].is_phase_field = True
+
+ self.assertEqual(
+ self.task.FetchAndAssertProjectInfo(mr),
+ fltconversion.ProjectInfo(
+ self.config, fltconversion.QUERY_MAP['default'],
+ template.approval_values, template.phases, 4, 5, 6, 9, 7, 8,
+ fltconversion.BROWSER_PHASE_MAP,
+ fltconversion.BROWSER_APPROVALS_TO_LABELS,
+ fltconversion.BROWSER_M_LABELS_RE))
+
+ # FINCH special case
+ # test approvals for Finch not required
+ mr = testing_helpers.MakeMonorailRequest(path='url/url?launch=finch')
+ self.assertRaisesRegexp(
+ AssertionError, 'finch template not set up correctly',
+ self.task.FetchAndAssertProjectInfo, mr)
+
+ for av in template.approval_values:
+ av.status = tracker_pb2.ApprovalStatus.NEEDS_REVIEW
+
+ self.assertEqual(
+ self.task.FetchAndAssertProjectInfo(mr),
+ fltconversion.ProjectInfo(
+ self.config, fltconversion.QUERY_MAP['finch'],
+ template.approval_values, template.phases, 4, 5, 6, 9, 7, 8,
+ fltconversion.BROWSER_PHASE_MAP,
+ fltconversion.BROWSER_APPROVALS_TO_LABELS,
+ fltconversion.BROWSER_M_LABELS_RE))
+
+ def testFetchAndAssertProjectInfo_OS(self):
+ self.task.services.project.GetProjectByName = mock.Mock()
+ self.task.services.config.GetProjectConfig = mock.Mock(
+ return_value=self.config)
+
+ mr = testing_helpers.MakeMonorailRequest(path='url/url?launch=os')
+ template = tracker_bizobj.MakeIssueTemplate(
+ 'template', 'sum', 'New', 111, 'content', [], [], [], [])
+ self.task.services.template.GetTemplateByName = mock.Mock(
+ return_value=template)
+
+ # test phases not recognized
+ template.phases = [tracker_pb2.Phase(name='Chrome-Test')]
+ template.approval_values = [tracker_pb2.ApprovalValue()]
+ self.assertRaisesRegexp(
+ AssertionError, 'one or more phases not recognized',
+ self.task.FetchAndAssertProjectInfo, mr)
+
+ template.phases = [tracker_pb2.Phase(name='feature freeze'),
+ tracker_pb2.Phase(name='branch')]
+
+ # test template not set up correctly
+ template.approval_values = [
+ tracker_pb2.ApprovalValue(approval_id=1),
+ tracker_pb2.ApprovalValue(approval_id=2),
+ tracker_pb2.ApprovalValue(approval_id=3)]
+ self.assertRaisesRegexp(
+ AssertionError, 'os template not set up correctly',
+ self.task.FetchAndAssertProjectInfo, mr)
+
+ for av in template.approval_values:
+ av.status = tracker_pb2.ApprovalStatus.NEEDS_REVIEW
+
+ # test approvals not recognized
+ self.assertRaisesRegexp(
+ AssertionError, 'one or more approvals not recognized',
+ self.task.FetchAndAssertProjectInfo, mr)
+
+ self.config.field_defs = [
+ tracker_pb2.FieldDef(field_id=1, field_name='ChromeOS-Enterprise',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE),
+ tracker_pb2.FieldDef(field_id=2, field_name='ChromeOS-UX',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE),
+ tracker_pb2.FieldDef(field_id=3, field_name='ChromeOS-Privacy',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE)
+ ]
+
+ # Skip remaining checks. No different from Browser process.
+ self.config.approval_defs = [
+ tracker_pb2.ApprovalDef(approval_id=1),
+ tracker_pb2.ApprovalDef(approval_id=2),
+ tracker_pb2.ApprovalDef(approval_id=3)]
+
+ self.config.field_defs.extend([
+ tracker_pb2.FieldDef(field_id=4, field_name='PM',
+ field_type=tracker_pb2.FieldTypes.USER_TYPE),
+ tracker_pb2.FieldDef(field_id=5, field_name='TL',
+ field_type=tracker_pb2.FieldTypes.USER_TYPE),
+ tracker_pb2.FieldDef(field_id=6, field_name='TE',
+ field_type=tracker_pb2.FieldTypes.USER_TYPE),
+ tracker_pb2.FieldDef(field_id=9, field_name='UX',
+ field_type=tracker_pb2.FieldTypes.USER_TYPE)
+ ])
+ self.config.field_defs.extend([
+ tracker_pb2.FieldDef(
+ field_id=7, field_name='M-Target', is_phase_field=True,
+ is_multivalued=True, field_type=tracker_pb2.FieldTypes.INT_TYPE),
+ tracker_pb2.FieldDef(
+ field_id=8, field_name='M-Approved', is_phase_field=True,
+ is_multivalued=True, field_type=tracker_pb2.FieldTypes.INT_TYPE)
+ ])
+
+ self.assertEqual(
+ self.task.FetchAndAssertProjectInfo(mr),
+ fltconversion.ProjectInfo(
+ self.config, fltconversion.QUERY_MAP['os'],
+ template.approval_values, template.phases, 4, 5, 6, 9, 7, 8,
+ fltconversion.OS_PHASE_MAP, fltconversion.OS_APPROVALS_TO_LABELS,
+ fltconversion.OS_M_LABELS_RE))
+
+ @mock.patch('time.time')
+ def testExecuteIssueChanges(self, mockTime):
+ mockTime.return_value = 123
+ self.task.services.issue._UpdateIssuesApprovals = mock.Mock()
+ self.task.services.issue.DeltaUpdateIssue = mock.Mock(
+ return_value=([], None))
+ self.task.services.issue.InsertComment = mock.Mock()
+ self.config.approval_defs = [
+ tracker_pb2.ApprovalDef(
+ # test empty survey
+ approval_id=1, survey='', approver_ids=[111, 222]),
+ tracker_pb2.ApprovalDef(approval_id=2), # test missing survey
+ tracker_pb2.ApprovalDef(survey='Missing approval_id should not error.'),
+ tracker_pb2.ApprovalDef(approval_id=3, survey='Q1\nQ2\n\nQ3'),
+ tracker_pb2.ApprovalDef(approval_id=4, survey='Q1\nQ2\n\nQ3 two'),
+ tracker_pb2.ApprovalDef()]
+
+ new_avs = [tracker_pb2.ApprovalValue(
+ approval_id=1, status=tracker_pb2.ApprovalStatus.APPROVED),
+ tracker_pb2.ApprovalValue(approval_id=4),
+ tracker_pb2.ApprovalValue(approval_id=2),
+ tracker_pb2.ApprovalValue(approval_id=3)]
+
+ phases = [tracker_pb2.Phase(phase_id=1, name='Phase1', rank=1)]
+ new_fvs = [tracker_bizobj.MakeFieldValue(
+ 11, 70, None, None, None, None, False, phase_id=1),
+ tracker_bizobj.MakeFieldValue(
+ 12, None, 'strfield', None, None, None, False)]
+ _amendments = self.task.ExecuteIssueChanges(
+ self.config, self.issue, new_avs, phases, new_fvs)
+
+ # approver_ids set in ExecuteIssueChanges()
+ new_avs[0].approver_ids = [111, 222]
+ self.issue.approval_values = new_avs
+ self.issue.phases = phases
+ delta = tracker_pb2.IssueDelta(
+ labels_add=['Type-FLT-Launch', 'FLT-Conversion'],
+ labels_remove=['Type-Launch'], field_vals_add=new_fvs)
+ cmt_1 = tracker_pb2.IssueComment(
+ issue_id=78901, project_id=789, user_id=self.mr.auth.user_id,
+ content='', is_description=True, approval_id=1, timestamp=123)
+ cmt_2 = tracker_pb2.IssueComment(
+ issue_id=78901, project_id=789, user_id=self.mr.auth.user_id,
+ content='', is_description=True, approval_id=2, timestamp=123)
+ cmt_3 = tracker_pb2.IssueComment(
+ issue_id=78901, project_id=789, user_id=self.mr.auth.user_id,
+ content='<b>Q1</b>\n<b>Q2</b>\n<b></b>\n<b>Q3</b>',
+ is_description=True, approval_id=3, timestamp=123)
+ cmt_4 = tracker_pb2.IssueComment(
+ issue_id=78901, project_id=789, user_id=self.mr.auth.user_id,
+ content='<b>Q1</b>\n<b>Q2</b>\n<b></b>\n<b>Q3 two</b>',
+ is_description=True, approval_id=4, timestamp=123)
+
+
+ comment_calls = [mock.call(self.mr.cnxn, cmt_1),
+ mock.call(self.mr.cnxn, cmt_4),
+ mock.call(self.mr.cnxn, cmt_2),
+ mock.call(self.mr.cnxn, cmt_3)]
+ self.task.services.issue.InsertComment.assert_has_calls(comment_calls)
+
+ self.task.services.issue._UpdateIssuesApprovals.assert_called_once_with(
+ self.mr.cnxn, self.issue)
+ self.task.services.issue.DeltaUpdateIssue.assert_called_once_with(
+ self.mr.cnxn, self.task.services, self.mr.auth.user_id, 789,
+ self.config, self.issue, delta,
+ comment=fltconversion.CONVERSION_COMMENT)
+
+ def testConvertPeopleLabels(self):
+ self.task.services.user.LookupUserID = mock.Mock(
+ side_effect=[1, 2, 3, 4, 5, 6])
+ labels = [
+ 'pm-u1', 'pm-u2', 'tl-u2', 'test-3', 'test-4', 'ux-u5', 'ux-6']
+ fvs = self.task.ConvertPeopleLabels(self.mr, labels, 11, 12, 13, 14)
+ expected = [
+ tracker_bizobj.MakeFieldValue(11, None, None, 1, None, None, False),
+ tracker_bizobj.MakeFieldValue(12, None, None, 2, None, None, False),
+ tracker_bizobj.MakeFieldValue(13, None, None, 3, None, None, False),
+ tracker_bizobj.MakeFieldValue(13, None, None, 4, None, None, False),
+ tracker_bizobj.MakeFieldValue(14, None, None, 5, None, None, False),
+ tracker_bizobj.MakeFieldValue(14, None, None, 6, None, None, False),
+ ]
+ self.assertEqual(fvs, expected)
+
+ def testConvertPeopleLabels_NoUsers(self):
+ def side_effect(_cnxn, _email):
+ raise exceptions.NoSuchUserException()
+ labels = []
+ self.task.services.user.LookupUserID = mock.Mock(side_effect=side_effect)
+ self.assertFalse(
+ len(self.task.ConvertPeopleLabels(self.mr, labels, 11, 12, 13, 14)))
+
+ def testCreateUserFieldValue_Chromium(self):
+ self.task.services.user.LookupUserID = mock.Mock(return_value=1)
+ actual = self.task.CreateUserFieldValue(self.mr, 'ldap', 11)
+ expected = tracker_bizobj.MakeFieldValue(
+ 11, None, None, 1, None, None, False)
+ self.assertEqual(actual, expected)
+ self.task.services.user.LookupUserID.assert_called_once_with(
+ self.mr.cnxn, 'ldap@chromium.org')
+
+ def testCreateUserFieldValue_Goog(self):
+ def side_effect(_cnxn, email):
+ if email.endswith('chromium.org'):
+ raise exceptions.NoSuchUserException()
+ else:
+ return 2
+ self.task.services.user.LookupUserID = mock.Mock(side_effect=side_effect)
+ actual = self.task.CreateUserFieldValue(self.mr, 'ldap', 11)
+ expected = tracker_bizobj.MakeFieldValue(
+ 11, None, None, 2, None, None, False)
+ self.assertEqual(actual, expected)
+ self.task.services.user.LookupUserID.assert_any_call(
+ self.mr.cnxn, 'ldap@chromium.org')
+ self.task.services.user.LookupUserID.assert_any_call(
+ self.mr.cnxn, 'ldap@google.com')
+
+ def testCreateUserFieldValue_NoUserFound(self):
+ def side_effect(_cnxn, _email):
+ raise exceptions.NoSuchUserException()
+ self.task.services.user.LookupUserID = mock.Mock(side_effect=side_effect)
+ self.assertIsNone(self.task.CreateUserFieldValue(self.mr, 'ldap', 11))
+
+
+class ConvertMLabels(unittest.TestCase):
+
+ def setUp(self):
+ self.target_id = 24
+ self.approved_id = 27
+ self.beta_phase = tracker_pb2.Phase(phase_id=1, name='bEtA')
+ self.stable_phase = tracker_pb2.Phase(phase_id=2, name='StAbLe')
+ self.stable_full_phase = tracker_pb2.Phase(phase_id=3, name='stable-FULL')
+ self.stable_exp_phase = tracker_pb2.Phase(phase_id=4, name='STABLE-exp')
+ self.feature_freeze_phase = tracker_pb2.Phase(
+ phase_id=5, name='FEATURE Freeze')
+ self.branch_phase = tracker_pb2.Phase(phase_id=6, name='bRANCH')
+
+ def testConvertMLabels_NormalFinch(self):
+
+ phases = [self.stable_exp_phase, self.beta_phase, self.stable_full_phase]
+ labels = [
+ 'launch-m-approved-81-beta', # beta:M-Approved=81
+ 'launch-m-target-80-stable-car', # ignore
+ 'a-Launch-M-Target-80-Stable-car', # ignore
+ 'launch-m-target-70-Stable', # stable-full:M-Target=70
+ 'LAUNCH-M-TARGET-71-STABLE', # stable-full:M-Target=71
+ 'launch-m-target-70-stable-exp', # stable-exp:M-Target=70
+ 'launch-m-target-69-stable-exp', # stable-exp:M-Target=69
+ 'launch-M-APPROVED-70-Stable-Exp', # stable-exp:M-Approved-70
+ 'launch-m-approved-73-stable', # stable-full:M-Approved-73
+ 'launch-m-error-73-stable', # ignore
+ 'launch-m-approved-8-stable', #ignore
+ 'irrelevant label-weird', # ignore
+ ]
+ actual_fvs = fltconversion.ConvertMLabels(
+ labels, phases, self.target_id, self.approved_id,
+ fltconversion.BROWSER_M_LABELS_RE, fltconversion.BROWSER_PHASE_MAP)
+
+ expected_fvs = [
+ tracker_pb2.FieldValue(
+ field_id=self.approved_id, int_value=81,
+ phase_id=self.beta_phase.phase_id, derived=False,),
+ tracker_pb2.FieldValue(
+ field_id=self.target_id, int_value=70,
+ phase_id=self.stable_full_phase.phase_id, derived=False),
+ tracker_pb2.FieldValue(
+ field_id=self.target_id, int_value=71,
+ phase_id=self.stable_full_phase.phase_id, derived=False),
+ tracker_pb2.FieldValue(
+ field_id=self.target_id, int_value=70,
+ phase_id=self.stable_exp_phase.phase_id, derived=False),
+ tracker_pb2.FieldValue(
+ field_id=self.target_id, int_value=69,
+ phase_id=self.stable_exp_phase.phase_id, derived=False),
+ tracker_pb2.FieldValue(
+ field_id=self.approved_id, int_value=70,
+ phase_id=self.stable_exp_phase.phase_id, derived=False),
+ tracker_pb2.FieldValue(
+ field_id=self.approved_id, int_value=73,
+ phase_id=self.stable_full_phase.phase_id, derived=False)
+ ]
+
+ self.assertEqual(actual_fvs, expected_fvs)
+
+ def testConvertMLabels_OS(self):
+ phases = [self.feature_freeze_phase, self.branch_phase, self.stable_phase]
+ labels = [
+ 'launch-m-approved-81-beta', # ignore
+ 'launch-m-target-80-stable-car', # ignore
+ 'a-Launch-M-Target-80-Stable-car', # ignore
+ 'launch-m-target-70-Stable', # stable:M-Target=70
+ 'LAUNCH-M-TARGET-71-STABLE', # stable:M-Target=71
+ 'launch-m-target-70-stable-exp', # ignore
+ 'launch-M-APPROVED-70-Stable-Exp', # ignore
+ 'launch-m-approved-73-stable', # stable:M-Approved-73
+ 'launch-m-error-73-stable', # ignore
+ 'launch-m-approved-8-stable', #ignore
+ 'irrelevant label-weird', # ignore
+ ]
+ actual_fvs = fltconversion.ConvertMLabels(
+ labels, phases, self.target_id, self.approved_id,
+ fltconversion.OS_M_LABELS_RE, fltconversion.OS_PHASE_MAP)
+
+ expected_fvs = [
+ tracker_pb2.FieldValue(
+ field_id=self.target_id, int_value=70,
+ phase_id=self.stable_phase.phase_id, derived=False,),
+ tracker_pb2.FieldValue(
+ field_id=self.target_id, int_value=71,
+ phase_id=self.stable_phase.phase_id, derived=False),
+ tracker_pb2.FieldValue(
+ field_id=self.approved_id, int_value=73,
+ phase_id=self.stable_phase.phase_id, derived=False)
+ ]
+
+ self.assertEqual(actual_fvs, expected_fvs)
+
+
+class ConvertLaunchLabels(unittest.TestCase):
+
+ def setUp(self):
+ self.project_fds = [
+ tracker_pb2.FieldDef(
+ field_id=1, project_id=789, field_name='String',
+ field_type=tracker_pb2.FieldTypes.STR_TYPE),
+ tracker_pb2.FieldDef(
+ field_id=2, project_id=789, field_name='Chrome-UX',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE),
+ tracker_pb2.FieldDef(
+ field_id=3, project_id=789, field_name='Chrome-Privacy',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE)
+ ]
+ approvalUX = tracker_pb2.ApprovalValue(
+ approval_id=2, status=tracker_pb2.ApprovalStatus.NEEDS_REVIEW)
+ approvalPrivacy = tracker_pb2.ApprovalValue(approval_id=3)
+ self.approvals = [approvalUX, approvalPrivacy]
+
+ def testConvertLaunchLabels_Normal(self):
+ labels = [
+ 'Launch-UX-NotReviewed', 'Launch-Privacy-Yes', 'Launch-NotRelevant']
+ actual = fltconversion.ConvertLaunchLabels(
+ labels, self.approvals, self.project_fds,
+ fltconversion.BROWSER_APPROVALS_TO_LABELS)
+ expected = [
+ tracker_pb2.ApprovalValue(
+ approval_id=2, status=tracker_pb2.ApprovalStatus.NEEDS_REVIEW),
+ tracker_pb2.ApprovalValue(
+ approval_id=3, status=tracker_pb2.ApprovalStatus.APPROVED)
+ ]
+ self.assertEqual(actual, expected)
+
+ def testConvertLaunchLabels_ExtraAndMissingLabels(self):
+ labels = [
+ 'Blah-Launch-Privacy-Yes', # Missing, this is not a valid Label
+ 'Launch-Security-Yes', # Extra, no matching approval in given approvals
+ 'Launch-UI-Yes'] # Missing Launch-Privacy
+ actual = fltconversion.ConvertLaunchLabels(
+ labels, self.approvals, self.project_fds,
+ fltconversion.BROWSER_APPROVALS_TO_LABELS)
+ expected = [
+ tracker_pb2.ApprovalValue(
+ approval_id=2, status=tracker_pb2.ApprovalStatus.APPROVED),
+ tracker_pb2.ApprovalValue(
+ approval_id=3, status=tracker_pb2.ApprovalStatus.NOT_SET)
+ ]
+ self.assertEqual(actual, expected)
+
+class ExtractLabelLDAPs(unittest.TestCase):
+
+ def testExtractLabelLDAPs_Normal(self):
+ labels = [
+ 'tl-USER1',
+ 'pm-',
+ 'tL-User2',
+ 'test-user4',
+ 'PM-USER3',
+ 'pm',
+ 'test-user5',
+ 'test-',
+ 'ux-user9']
+ (actual_pm, actual_tl, actual_tests,
+ actual_ux) = fltconversion.ExtractLabelLDAPs(labels)
+ self.assertEqual(actual_pm, 'user3')
+ self.assertEqual(actual_tl, 'user2')
+ self.assertEqual(actual_tests, ['user4', 'user5'])
+ self.assertEqual(actual_ux, ['user9'])
+
+ def testExtractLabelLDAPs_NoLabels(self):
+ (actual_pm, actual_tl, actual_tests,
+ actual_ux) = fltconversion.ExtractLabelLDAPs([])
+ self.assertIsNone(actual_pm)
+ self.assertIsNone(actual_tl)
+ self.assertFalse(len(actual_tests))
+ self.assertFalse(len(actual_ux))
diff --git a/tracker/test/issueadmin_test.py b/tracker/test/issueadmin_test.py
new file mode 100644
index 0000000..751e414
--- /dev/null
+++ b/tracker/test/issueadmin_test.py
@@ -0,0 +1,464 @@
+# Copyright 2016 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 issue admin pages."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mox
+import unittest
+
+from mock import Mock, patch
+
+from framework import permissions
+from framework import urls
+from proto import tracker_pb2
+from services import service_manager
+from services import template_svc
+from testing import fake
+from testing import testing_helpers
+from tracker import issueadmin
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+
+
+class TestBase(unittest.TestCase):
+
+ def setUpServlet(self, servlet_factory):
+ # pylint: disable=attribute-defined-outside-init
+ self.services = service_manager.Services(
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ user=fake.UserService(),
+ issue=fake.IssueService(),
+ template=Mock(spec=template_svc.TemplateService),
+ features=fake.FeaturesService())
+ self.servlet = servlet_factory('req', 'res', services=self.services)
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=789, contrib_ids=[333])
+ self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ self.services.config.StoreConfig(None, self.config)
+ self.cnxn = fake.MonorailConnection()
+ self.mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/admin', project=self.project)
+ self.mox = mox.Mox()
+ self.test_template = tracker_bizobj.MakeIssueTemplate(
+ 'Test Template', 'sum', 'New', 111, 'content', [], [], [], [])
+ self.test_template.template_id = 12345
+ self.test_templates = testing_helpers.DefaultTemplates()
+ self.test_templates.append(self.test_template)
+ self.services.template.GetProjectTemplates\
+ .return_value = self.test_templates
+ self.services.template.GetTemplateSetForProject\
+ .return_value = [(12345, 'Test template', 0)]
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def _mockGetUser(self):
+ self.mox.StubOutWithMock(self.services.user, 'GetUser')
+ user = self.services.user.TestAddUser('user@invalid', 100)
+ self.services.user.GetUser(
+ mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(user)
+
+
+class IssueAdminBaseTest(TestBase):
+
+ def setUp(self):
+ super(IssueAdminBaseTest, self).setUpServlet(issueadmin.IssueAdminBase)
+
+ def testGatherPageData(self):
+ self._mockGetUser()
+ self.mox.ReplayAll()
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.mox.VerifyAll()
+
+ self.assertItemsEqual(
+ ['admin_tab_mode', 'config', 'open_text', 'closed_text', 'labels_text'],
+ list(page_data.keys()))
+ config_view = page_data['config']
+ self.assertEqual(789, config_view.project_id)
+
+
+class AdminStatusesTest(TestBase):
+
+ def setUp(self):
+ super(AdminStatusesTest, self).setUpServlet(issueadmin.AdminStatuses)
+
+ @patch('framework.servlet.Servlet.PleaseCorrect')
+ def testProcessSubtabForm_MissingInput(self, mock_pc):
+ post_data = fake.PostData()
+ next_url = self.servlet.ProcessSubtabForm(post_data, self.mr)
+ self.assertIsNone(next_url)
+ mock_pc.assert_called_once()
+ self.assertEqual(len(tracker_constants.DEFAULT_WELL_KNOWN_STATUSES),
+ len(self.config.well_known_statuses))
+ self.assertEqual(tracker_constants.DEFAULT_STATUSES_OFFER_MERGE,
+ self.config.statuses_offer_merge)
+
+ @patch('framework.servlet.Servlet.PleaseCorrect')
+ def testProcessSubtabForm_EmptyInput(self, mock_pc):
+ post_data = fake.PostData(
+ predefinedopen=[''], predefinedclosed=[''], statuses_offer_merge=[''])
+ next_url = self.servlet.ProcessSubtabForm(post_data, self.mr)
+ self.assertIsNone(next_url)
+ mock_pc.assert_called_once()
+ self.assertEqual(len(tracker_constants.DEFAULT_WELL_KNOWN_STATUSES),
+ len(self.config.well_known_statuses))
+ self.assertEqual(tracker_constants.DEFAULT_STATUSES_OFFER_MERGE,
+ self.config.statuses_offer_merge)
+
+ def testProcessSubtabForm_Normal(self):
+ post_data = fake.PostData(
+ predefinedopen=['New = newly reported'],
+ predefinedclosed=['Fixed\nDuplicate'],
+ statuses_offer_merge=['Duplicate'])
+ next_url = self.servlet.ProcessSubtabForm(post_data, self.mr)
+ self.assertEqual(urls.ADMIN_STATUSES, next_url)
+ self.assertEqual(3, len(self.config.well_known_statuses))
+ self.assertEqual('New', self.config.well_known_statuses[0].status)
+ self.assertTrue(self.config.well_known_statuses[0].means_open)
+ self.assertEqual('Fixed', self.config.well_known_statuses[1].status)
+ self.assertFalse(self.config.well_known_statuses[1].means_open)
+ self.assertEqual('Duplicate', self.config.well_known_statuses[2].status)
+ self.assertFalse(self.config.well_known_statuses[2].means_open)
+ self.assertEqual(['Duplicate'], self.config.statuses_offer_merge)
+
+
+class AdminLabelsTest(TestBase):
+
+ def setUp(self):
+ super(AdminLabelsTest, self).setUpServlet(issueadmin.AdminLabels)
+
+ def testGatherPageData(self):
+ self._mockGetUser()
+ self.mox.ReplayAll()
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.mox.VerifyAll()
+
+ self.assertItemsEqual(
+ ['admin_tab_mode', 'config', 'field_defs',
+ 'open_text', 'closed_text', 'labels_text'],
+ list(page_data.keys()))
+ config_view = page_data['config']
+ self.assertEqual(789, config_view.project_id)
+ self.assertEqual([], page_data['field_defs'])
+
+ @patch('framework.servlet.Servlet.PleaseCorrect')
+ def testProcessSubtabForm_MissingInput(self, mock_pc):
+ post_data = fake.PostData()
+ next_url = self.servlet.ProcessSubtabForm(post_data, self.mr)
+ self.assertIsNone(next_url)
+ mock_pc.assert_called_once()
+ self.assertEqual(len(tracker_constants.DEFAULT_WELL_KNOWN_LABELS),
+ len(self.config.well_known_labels))
+ self.assertEqual(tracker_constants.DEFAULT_EXCL_LABEL_PREFIXES,
+ self.config.exclusive_label_prefixes)
+
+ @patch('framework.servlet.Servlet.PleaseCorrect')
+ def testProcessSubtabForm_EmptyInput(self, mock_pc):
+ post_data = fake.PostData(
+ predefinedlabels=[''], excl_prefixes=[''])
+ next_url = self.servlet.ProcessSubtabForm(post_data, self.mr)
+ self.assertIsNone(next_url) # Because PleaseCorrect() was called.
+ mock_pc.assert_called_once()
+ self.assertEqual(len(tracker_constants.DEFAULT_WELL_KNOWN_LABELS),
+ len(self.config.well_known_labels))
+ self.assertEqual(tracker_constants.DEFAULT_EXCL_LABEL_PREFIXES,
+ self.config.exclusive_label_prefixes)
+
+ def testProcessSubtabForm_Normal(self):
+ post_data = fake.PostData(
+ predefinedlabels=['Pri-0 = Burning issue\nPri-4 = It can wait'],
+ excl_prefixes=['pri'])
+ next_url = self.servlet.ProcessSubtabForm(post_data, self.mr)
+ self.assertEqual(urls.ADMIN_LABELS, next_url)
+ self.assertEqual(2, len(self.config.well_known_labels))
+ self.assertEqual('Pri-0', self.config.well_known_labels[0].label)
+ self.assertEqual('Pri-4', self.config.well_known_labels[1].label)
+ self.assertEqual(['pri'], self.config.exclusive_label_prefixes)
+
+ @patch('framework.servlet.Servlet.PleaseCorrect')
+ def testProcessSubtabForm_Duplicates(self, mock_pc):
+ post_data = fake.PostData(
+ predefinedlabels=['Pri-0\nPri-4\npri-0'],
+ excl_prefixes=['pri'])
+ next_url = self.servlet.ProcessSubtabForm(post_data, self.mr)
+ self.assertIsNone(next_url)
+ mock_pc.assert_called_once()
+ self.assertEqual(
+ 'Duplicate label: pri-0',
+ self.mr.errors.label_defs)
+
+ @patch('framework.servlet.Servlet.PleaseCorrect')
+ def testProcessSubtabForm_Conflict(self, mock_pc):
+ post_data = fake.PostData(
+ predefinedlabels=['Multi-Part-One\nPri-4\npri-0'],
+ excl_prefixes=['pri'])
+ self.config.field_defs = [
+ tracker_pb2.FieldDef(
+ field_name='Multi-Part',
+ field_type=tracker_pb2.FieldTypes.ENUM_TYPE)]
+ next_url = self.servlet.ProcessSubtabForm(post_data, self.mr)
+ self.assertIsNone(next_url)
+ mock_pc.assert_called_once()
+ self.assertEqual(
+ 'Label "Multi-Part-One" should be defined in enum "multi-part"',
+ self.mr.errors.label_defs)
+
+
+class AdminTemplatesTest(TestBase):
+
+ def setUp(self):
+ super(AdminTemplatesTest, self).setUpServlet(issueadmin.AdminTemplates)
+ self.mr.auth.user_id = 333
+ self.mr.auth.effective_ids = {333}
+
+ def testGatherPageData(self):
+ self._mockGetUser()
+ self.mox.ReplayAll()
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.mox.VerifyAll()
+
+ config_view = page_data['config']
+ self.assertEqual(789, config_view.project_id)
+
+ def testProcessSubtabForm_NoEditProjectPerm(self):
+ """If user lacks perms, raise an exception."""
+ post_data = fake.PostData(
+ default_template_for_developers=['Test Template'],
+ default_template_for_users=['Test Template'])
+ self.mr.perms = permissions.EMPTY_PERMISSIONSET
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.ProcessSubtabForm, post_data, self.mr)
+ self.assertEqual(0, self.config.default_template_for_developers)
+ self.assertEqual(0, self.config.default_template_for_users)
+
+ def testProcessSubtabForm_Normal(self):
+ """If user has perms, set default templates."""
+ post_data = fake.PostData(
+ default_template_for_developers=['Test Template'],
+ default_template_for_users=['Test Template'])
+ next_url = self.servlet.ProcessSubtabForm(post_data, self.mr)
+ self.assertEqual(urls.ADMIN_TEMPLATES, next_url)
+ self.assertEqual(12345, self.config.default_template_for_developers)
+ self.assertEqual(12345, self.config.default_template_for_users)
+
+ def testParseDefaultTemplateSelections_NotSpecified(self):
+ post_data = fake.PostData()
+ for_devs, for_users = self.servlet._ParseDefaultTemplateSelections(
+ post_data, self.test_templates)
+ self.assertEqual(None, for_devs)
+ self.assertEqual(None, for_users)
+
+ def testParseDefaultTemplateSelections_TemplateNotFoundIsIgnored(self):
+ post_data = fake.PostData(
+ default_template_for_developers=['Bad value'],
+ default_template_for_users=['Bad value'])
+ for_devs, for_users = self.servlet._ParseDefaultTemplateSelections(
+ post_data, self.test_templates)
+ self.assertEqual(None, for_devs)
+ self.assertEqual(None, for_users)
+
+ def testParseDefaultTemplateSelections_Normal(self):
+ post_data = fake.PostData(
+ default_template_for_developers=['Test Template'],
+ default_template_for_users=['Test Template'])
+ for_devs, for_users = self.servlet._ParseDefaultTemplateSelections(
+ post_data, self.test_templates)
+ self.assertEqual(12345, for_devs)
+ self.assertEqual(12345, for_users)
+
+
+class AdminComponentsTest(TestBase):
+
+ def setUp(self):
+ super(AdminComponentsTest, self).setUpServlet(issueadmin.AdminComponents)
+ self.cd_clean = tracker_bizobj.MakeComponentDef(
+ 1, self.project.project_id, 'BackEnd', 'doc', False, [], [111], 100000,
+ 122, 10000000, 133)
+ self.cd_with_subcomp = tracker_bizobj.MakeComponentDef(
+ 2, self.project.project_id, 'FrontEnd', 'doc', False, [], [111],
+ 100000, 122, 10000000, 133)
+ self.subcd = tracker_bizobj.MakeComponentDef(
+ 3, self.project.project_id, 'FrontEnd>Worker', 'doc', False, [], [111],
+ 100000, 122, 10000000, 133)
+ self.cd_with_template = tracker_bizobj.MakeComponentDef(
+ 4, self.project.project_id, 'Middle', 'doc', False, [], [111],
+ 100000, 122, 10000000, 133)
+
+ def testGatherPageData(self):
+ self._mockGetUser()
+ self.mox.ReplayAll()
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.mox.VerifyAll()
+ self.assertItemsEqual(
+ ['admin_tab_mode', 'failed_templ', 'component_defs', 'failed_perm',
+ 'config', 'failed_subcomp',
+ 'open_text', 'closed_text', 'labels_text'],
+ list(page_data.keys()))
+ config_view = page_data['config']
+ self.assertEqual(789, config_view.project_id)
+ self.assertEqual([], page_data['component_defs'])
+
+ def testProcessFormData_NoErrors(self):
+ self.config.component_defs = [
+ self.cd_clean, self.cd_with_subcomp, self.subcd, self.cd_with_template]
+ self.services.template.TemplatesWithComponent.return_value = []
+ post_data = {
+ 'delete_components' : '%s,%s,%s' % (
+ self.cd_clean.path, self.cd_with_subcomp.path, self.subcd.path)}
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertTrue(
+ url.startswith('http://127.0.0.1/p/proj/adminComponents?deleted='
+ 'FrontEnd%3EWorker%2CFrontEnd%2CBackEnd&failed_perm=&'
+ 'failed_subcomp=&failed_templ=&ts='))
+
+ def testProcessFormData_SubCompError(self):
+ self.config.component_defs = [
+ self.cd_clean, self.cd_with_subcomp, self.subcd, self.cd_with_template]
+ self.services.template.TemplatesWithComponent.return_value = []
+ post_data = {
+ 'delete_components' : '%s,%s' % (
+ self.cd_clean.path, self.cd_with_subcomp.path)}
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertTrue(
+ url.startswith('http://127.0.0.1/p/proj/adminComponents?deleted='
+ 'BackEnd&failed_perm=&failed_subcomp=FrontEnd&'
+ 'failed_templ=&ts='))
+
+ def testProcessFormData_TemplateError(self):
+ self.config.component_defs = [
+ self.cd_clean, self.cd_with_subcomp, self.subcd, self.cd_with_template]
+
+ def mockTemplatesWithComponent(_cnxn, component_id):
+ if component_id == 4:
+ return 'template'
+ self.services.template.TemplatesWithComponent\
+ .side_effect = mockTemplatesWithComponent
+
+ post_data = {
+ 'delete_components' : '%s,%s,%s,%s' % (
+ self.cd_clean.path, self.cd_with_subcomp.path, self.subcd.path,
+ self.cd_with_template.path)}
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertTrue(
+ url.startswith('http://127.0.0.1/p/proj/adminComponents?deleted='
+ 'FrontEnd%3EWorker%2CFrontEnd%2CBackEnd&failed_perm=&'
+ 'failed_subcomp=&failed_templ=Middle&ts='))
+
+
+class AdminViewsTest(TestBase):
+
+ def setUp(self):
+ super(AdminViewsTest, self).setUpServlet(issueadmin.AdminViews)
+
+ def testGatherPageData(self):
+ self._mockGetUser()
+ self.mox.ReplayAll()
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.mox.VerifyAll()
+
+ self.assertItemsEqual(
+ ['canned_queries', 'admin_tab_mode', 'config', 'issue_notify',
+ 'new_query_indexes', 'max_queries',
+ 'open_text', 'closed_text', 'labels_text'],
+ list(page_data.keys()))
+ config_view = page_data['config']
+ self.assertEqual(789, config_view.project_id)
+
+ def testProcessSubtabForm(self):
+ post_data = fake.PostData(
+ default_col_spec=['id pri mstone owner status summary'],
+ default_sort_spec=['mstone pri'],
+ default_x_attr=['owner'], default_y_attr=['mstone'])
+ next_url = self.servlet.ProcessSubtabForm(post_data, self.mr)
+ self.assertEqual(urls.ADMIN_VIEWS, next_url)
+ self.assertEqual(
+ 'id pri mstone owner status summary', self.config.default_col_spec)
+ self.assertEqual('mstone pri', self.config.default_sort_spec)
+ self.assertEqual('owner', self.config.default_x_attr)
+ self.assertEqual('mstone', self.config.default_y_attr)
+
+
+class AdminViewsFunctionsTest(unittest.TestCase):
+
+ def testParseListPreferences(self):
+ # If no input, col_spec will be default column spec.
+ # For other fiels empty strings should be returned.
+ (col_spec, sort_spec, x_attr, y_attr, member_default_query,
+ ) = issueadmin._ParseListPreferences({})
+ self.assertEqual(tracker_constants.DEFAULT_COL_SPEC, col_spec)
+ self.assertEqual('', sort_spec)
+ self.assertEqual('', x_attr)
+ self.assertEqual('', y_attr)
+ self.assertEqual('', member_default_query)
+
+ # Test how hyphens in input are treated.
+ spec = 'label1-sub1 label2 label3-sub3'
+ (col_spec, sort_spec, x_attr, y_attr, member_default_query,
+ ) = issueadmin._ParseListPreferences(
+ fake.PostData(default_col_spec=[spec],
+ default_sort_spec=[spec],
+ default_x_attr=[spec],
+ default_y_attr=[spec]),
+ )
+
+ # Hyphens (and anything following) should be stripped from each term.
+ self.assertEqual('label1-sub1 label2 label3-sub3', col_spec)
+
+ # The sort spec should be as given (except with whitespace condensed).
+ self.assertEqual(' '.join(spec.split()), sort_spec)
+
+ # Only the first term (up to the first hyphen) should be used for x- or
+ # y-attr.
+ self.assertEqual('label1-sub1', x_attr)
+ self.assertEqual('label1-sub1', y_attr)
+
+ # Test that multibyte strings are not mangled.
+ spec = ('\xe7\xaa\xbf\xe8\x8b\xa5-\xe7\xb9\xb9 '
+ '\xe5\x9c\xb0\xe3\x81\xa6-\xe5\xbd\x93-\xe3\x81\xbe\xe3\x81\x99')
+ spec = spec.decode('utf-8')
+ (col_spec, sort_spec, x_attr, y_attr, member_default_query,
+ ) = issueadmin._ParseListPreferences(
+ fake.PostData(default_col_spec=[spec],
+ default_sort_spec=[spec],
+ default_x_attr=[spec],
+ default_y_attr=[spec],
+ member_default_query=[spec]),
+ )
+ self.assertEqual(spec, col_spec)
+ self.assertEqual(' '.join(spec.split()), sort_spec)
+ self.assertEqual('\xe7\xaa\xbf\xe8\x8b\xa5-\xe7\xb9\xb9'.decode('utf-8'),
+ x_attr)
+ self.assertEqual('\xe7\xaa\xbf\xe8\x8b\xa5-\xe7\xb9\xb9'.decode('utf-8'),
+ y_attr)
+ self.assertEqual(spec, member_default_query)
+
+
+class AdminRulesTest(TestBase):
+
+ def setUp(self):
+ super(AdminRulesTest, self).setUpServlet(issueadmin.AdminRules)
+
+ def testGatherPageData(self):
+ self._mockGetUser()
+ self.mox.ReplayAll()
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.mox.VerifyAll()
+
+ self.assertItemsEqual(
+ ['admin_tab_mode', 'config', 'rules', 'new_rule_indexes',
+ 'max_rules', 'open_text', 'closed_text', 'labels_text'],
+ list(page_data.keys()))
+ config_view = page_data['config']
+ self.assertEqual(789, config_view.project_id)
+ self.assertEqual([], page_data['rules'])
+
+ def testProcessSubtabForm(self):
+ pass # TODO(jrobbins): write this test
diff --git a/tracker/test/issueadvsearch_test.py b/tracker/test/issueadvsearch_test.py
new file mode 100644
index 0000000..fd1ee2e
--- /dev/null
+++ b/tracker/test/issueadvsearch_test.py
@@ -0,0 +1,78 @@
+# Copyright 2016 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 monorail.tracker.issueadvsearch."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import issueadvsearch
+
+class IssueAdvSearchTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ features=fake.FeaturesService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ project=fake.ProjectService())
+ self.project = self.services.project.TestAddProject('proj', project_id=987)
+ self.servlet = issueadvsearch.IssueAdvancedSearch(
+ 'req', 'res', services=self.services)
+
+ def testGatherData(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/advsearch')
+ page_data = self.servlet.GatherPageData(mr)
+
+ self.assertTrue('issue_tab_mode' in page_data)
+ self.assertTrue('page_perms' in page_data)
+
+ def testProcessFormData(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/advsearch')
+ post_data = {}
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.assertTrue('can=2' in url)
+
+ post_data['can'] = 42
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.assertTrue('can=42' in url)
+
+ post_data['starcount'] = 42
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.assertTrue('starcount%3A42' in url)
+
+ post_data['starcount'] = -1
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.assertTrue('starcount' not in url)
+
+ def _testAND(self, operator, field, post_data, query):
+ self.servlet._AccumulateANDTerm(operator, field, post_data, query)
+ return query
+
+ def test_AccumulateANDTerm(self):
+ query = self._testAND('', 'foo', {'foo': 'bar'}, [])
+ self.assertEqual(['bar'], query)
+
+ query = self._testAND('', 'bar', {'bar': 'baz=zippy'}, query)
+ self.assertEqual(['bar', 'baz', 'zippy'], query)
+
+ def _testOR(self, operator, field, post_data, query):
+ self.servlet._AccumulateORTerm(operator, field, post_data, query)
+ return query
+
+ def test_AccumulateORTerm(self):
+ query = self._testOR('', 'foo', {'foo': 'bar'}, [])
+ self.assertEqual(['bar'], query)
+
+ query = self._testOR('', 'bar', {'bar': 'baz=zippy'}, query)
+ self.assertEqual(['bar', 'baz,zippy'], query)
diff --git a/tracker/test/issueattachment_test.py b/tracker/test/issueattachment_test.py
new file mode 100644
index 0000000..8c65014
--- /dev/null
+++ b/tracker/test/issueattachment_test.py
@@ -0,0 +1,198 @@
+# Copyright 2016 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 monorail.tracker.issueattachment."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from google.appengine.api import images
+from google.appengine.ext import testbed
+
+import mox
+import webapp2
+
+from framework import gcs_helpers
+from framework import permissions
+from framework import servlet
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import attachment_helpers
+from tracker import issueattachment
+from tracker import tracker_helpers
+
+from third_party import cloudstorage
+
+
+class IssueattachmentTest(unittest.TestCase):
+
+ def setUp(self):
+ self.mox = mox.Mox()
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_app_identity_stub()
+ self.testbed.init_urlfetch_stub()
+ self.attachment_data = ""
+
+ self._old_gcs_open = cloudstorage.open
+ cloudstorage.open = fake.gcs_open
+
+ services = service_manager.Services(
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService())
+ self.project = services.project.TestAddProject('proj')
+ self.servlet = issueattachment.AttachmentPage(
+ 'req', webapp2.Response(), services=services)
+ services.user.TestAddUser('commenter@example.com', 111)
+ self.issue = fake.MakeTestIssue(
+ self.project.project_id, 1, 'summary', 'New', 111)
+ services.issue.TestAddIssue(self.issue)
+ self.comment = tracker_pb2.IssueComment(
+ id=123, issue_id=self.issue.issue_id,
+ project_id=self.project.project_id, user_id=111,
+ content='this is a comment')
+ services.issue.TestAddComment(self.comment, self.issue.local_id)
+ self.attachment = tracker_pb2.Attachment(
+ attachment_id=54321, filename='hello.txt', filesize=23432,
+ mimetype='text/plain', gcs_object_id='/pid/attachments/object_id')
+ services.issue.TestAddAttachment(
+ self.attachment, self.comment.id, self.issue.issue_id)
+ self.orig_sign_attachment_id = attachment_helpers.SignAttachmentID
+ attachment_helpers.SignAttachmentID = (
+ lambda aid: 'signed_%d' % aid)
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+ self.testbed.deactivate()
+ cloudstorage.open = self._old_gcs_open
+ attachment_helpers.SignAttachmentID = self.orig_sign_attachment_id
+
+ def testGatherPageData_NotFound(self):
+ aid = 12345
+ path = '/p/proj/issues/attachment?aid=%s&signed_aid=signed_%d' % (
+ aid, aid)
+ # But, no such attachment is in the database.
+ _request, mr = testing_helpers.GetRequestObjects(
+ project=self.project, path=path,
+ perms=permissions.EMPTY_PERMISSIONSET)
+ with self.assertRaises(webapp2.HTTPException) as cm:
+ self.servlet.GatherPageData(mr)
+ self.assertEqual(404, cm.exception.code)
+
+ # TODO(jrobbins): test cases for missing comment and missing issue.
+
+ def testGatherPageData_PermissionDenied(self):
+ aid = self.attachment.attachment_id
+ path = '/p/proj/issues/attachment?aid=%s&signed_aid=signed_%d' % (
+ aid, aid)
+ _request, mr = testing_helpers.GetRequestObjects(
+ project=self.project, path=path,
+ perms=permissions.EMPTY_PERMISSIONSET) # not even VIEW
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.GatherPageData, mr)
+
+ _request, mr = testing_helpers.GetRequestObjects(
+ project=self.project, path=path,
+ perms=permissions.READ_ONLY_PERMISSIONSET) # includes VIEW
+
+ # issue is now deleted
+ self.issue.deleted = True
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.GatherPageData, mr)
+ self.issue.deleted = False
+
+ # issue is now restricted
+ self.issue.labels.extend(['Restrict-View-PermYouLack'])
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.GatherPageData, mr)
+
+ def testGatherPageData_Download_WithDisposition(self):
+ aid = self.attachment.attachment_id
+ self.mox.StubOutWithMock(gcs_helpers, 'MaybeCreateDownload')
+ gcs_helpers.MaybeCreateDownload(
+ 'app_default_bucket',
+ '/pid/attachments/object_id',
+ self.attachment.filename).AndReturn(True)
+ self.mox.StubOutWithMock(gcs_helpers, 'SignUrl')
+ gcs_helpers.SignUrl(
+ 'app_default_bucket',
+ '/pid/attachments/object_id-download'
+ ).AndReturn('googleusercontent.com/...-download...')
+ self.mox.StubOutWithMock(self.servlet, 'redirect')
+ path = '/p/proj/issues/attachment?aid=%s&signed_aid=signed_%d' % (
+ aid, aid)
+ _request, mr = testing_helpers.GetRequestObjects(
+ project=self.project, path=path,
+ perms=permissions.READ_ONLY_PERMISSIONSET) # includes VIEW
+ self.servlet.redirect(
+ mox.And(mox.StrContains('googleusercontent.com'),
+ mox.StrContains('-download')), abort=True)
+ self.mox.ReplayAll()
+ self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
+
+ def testGatherPageData_Download_WithoutDisposition(self):
+ aid = self.attachment.attachment_id
+ path = '/p/proj/issues/attachment?aid=%s&signed_aid=signed_%d' % (
+ aid, aid)
+ self.mox.StubOutWithMock(gcs_helpers, 'MaybeCreateDownload')
+ gcs_helpers.MaybeCreateDownload(
+ 'app_default_bucket',
+ '/pid/attachments/object_id',
+ self.attachment.filename).AndReturn(False)
+ self.mox.StubOutWithMock(gcs_helpers, 'SignUrl')
+ gcs_helpers.SignUrl(
+ 'app_default_bucket',
+ '/pid/attachments/object_id'
+ ).AndReturn('googleusercontent.com/...')
+ self.mox.StubOutWithMock(self.servlet, 'redirect')
+ _request, mr = testing_helpers.GetRequestObjects(
+ project=self.project, path=path,
+ perms=permissions.READ_ONLY_PERMISSIONSET) # includes VIEW
+ self.servlet.redirect(
+ mox.And(mox.StrContains('googleusercontent.com'),
+ mox.Not(mox.StrContains('-download'))), abort=True)
+ self.mox.ReplayAll()
+ self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
+
+ def testGatherPageData_DownloadBadFilename(self):
+ aid = self.attachment.attachment_id
+ path = '/p/proj/issues/attachment?aid=%s&signed_aid=signed_%d' % (
+ aid, aid)
+ self.attachment.filename = '<script>alert("xsrf")</script>.txt';
+ safe_filename = 'attachment-%d.dat' % aid
+ self.mox.StubOutWithMock(gcs_helpers, 'MaybeCreateDownload')
+ gcs_helpers.MaybeCreateDownload(
+ 'app_default_bucket',
+ '/pid/attachments/object_id',
+ safe_filename).AndReturn(True)
+ self.mox.StubOutWithMock(gcs_helpers, 'SignUrl')
+ gcs_helpers.SignUrl(
+ 'app_default_bucket',
+ '/pid/attachments/object_id-download'
+ ).AndReturn('googleusercontent.com/...-download...')
+ self.mox.StubOutWithMock(self.servlet, 'redirect')
+ _request, mr = testing_helpers.GetRequestObjects(
+ project=self.project,
+ path=path,
+ perms=permissions.READ_ONLY_PERMISSIONSET) # includes VIEW
+ self.servlet.redirect(mox.And(
+ mox.Not(mox.StrContains(self.attachment.filename)),
+ mox.StrContains('googleusercontent.com')), abort=True)
+ self.mox.ReplayAll()
+ self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
diff --git a/tracker/test/issueattachmenttext_test.py b/tracker/test/issueattachmenttext_test.py
new file mode 100644
index 0000000..187aa42
--- /dev/null
+++ b/tracker/test/issueattachmenttext_test.py
@@ -0,0 +1,191 @@
+# Copyright 2016 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 issueattachmenttext."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import unittest
+from mock import patch
+
+from google.appengine.ext import testbed
+
+from third_party import cloudstorage
+import ezt
+
+import webapp2
+
+from framework import filecontent
+from framework import permissions
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import issueattachmenttext
+
+
+class IssueAttachmentTextTest(unittest.TestCase):
+
+ def setUp(self):
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_app_identity_stub()
+
+ services = service_manager.Services(
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService())
+ self.project = services.project.TestAddProject('proj')
+ self.servlet = issueattachmenttext.AttachmentText(
+ 'req', 'res', services=services)
+
+ services.user.TestAddUser('commenter@example.com', 111)
+
+ self.issue = tracker_pb2.Issue()
+ self.issue.local_id = 1
+ self.issue.issue_id = 1
+ self.issue.summary = 'sum'
+ self.issue.project_name = 'proj'
+ self.issue.project_id = self.project.project_id
+ services.issue.TestAddIssue(self.issue)
+
+ self.comment0 = tracker_pb2.IssueComment()
+ self.comment0.content = 'this is the description'
+ self.comment0.user_id = 111
+ self.comment1 = tracker_pb2.IssueComment()
+ self.comment1.content = 'this is a comment'
+ self.comment1.user_id = 111
+
+ self.attach0 = tracker_pb2.Attachment(
+ attachment_id=4567, filename='b.txt', mimetype='text/plain',
+ gcs_object_id='/pid/attachments/abcd')
+ self.comment0.attachments.append(self.attach0)
+
+ self.attach1 = tracker_pb2.Attachment(
+ attachment_id=1234, filename='a.txt', mimetype='text/plain',
+ gcs_object_id='/pid/attachments/abcdefg')
+ self.comment0.attachments.append(self.attach1)
+
+ self.bin_attach = tracker_pb2.Attachment(
+ attachment_id=2468, mimetype='application/octets',
+ gcs_object_id='/pid/attachments/\0\0\0\0\0\1\2\3')
+ self.comment1.attachments.append(self.bin_attach)
+
+ self.comment0.project_id = self.project.project_id
+ services.issue.TestAddComment(self.comment0, self.issue.local_id)
+ self.comment1.project_id = self.project.project_id
+ services.issue.TestAddComment(self.comment1, self.issue.local_id)
+ services.issue.TestAddAttachment(
+ self.attach0, self.comment0.id, self.issue.issue_id)
+ services.issue.TestAddAttachment(
+ self.attach1, self.comment1.id, self.issue.issue_id)
+ # TODO(jrobbins): add tests for binary content
+ self._old_gcs_open = cloudstorage.open
+ cloudstorage.open = fake.gcs_open
+
+ def tearDown(self):
+ self.testbed.deactivate()
+ cloudstorage.open = self._old_gcs_open
+
+ def testGatherPageData_CommentDeleted(self):
+ """If the attachment's comment was deleted, give a 403."""
+ _request, mr = testing_helpers.GetRequestObjects(
+ project=self.project,
+ path='/a/d.com/p/proj/issues/attachmentText?aid=1234',
+ perms=permissions.READ_ONLY_PERMISSIONSET)
+ self.servlet.GatherPageData(mr) # OK
+ self.comment1.deleted_by = 111
+ self.assertRaises( # 403
+ permissions.PermissionException,
+ self.servlet.GatherPageData, mr)
+
+ def testGatherPageData_IssueNotViewable(self):
+ """If the attachment's issue is not viewable, give a 403."""
+ _request, mr = testing_helpers.GetRequestObjects(
+ project=self.project,
+ path='/p/proj/issues/attachment?aid=1234',
+ perms=permissions.EMPTY_PERMISSIONSET) # No VIEW
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.GatherPageData, mr)
+
+ def testGatherPageData_IssueDeleted(self):
+ _request, mr = testing_helpers.GetRequestObjects(
+ project=self.project,
+ path='/p/proj/issues/attachment?aid=1234',
+ perms=permissions.READ_ONLY_PERMISSIONSET)
+ self.issue.deleted = True
+ self.assertRaises( # Issue was deleted
+ permissions.PermissionException,
+ self.servlet.GatherPageData, mr)
+
+ def testGatherPageData_IssueRestricted(self):
+ _request, mr = testing_helpers.GetRequestObjects(
+ project=self.project,
+ path='/p/proj/issues/attachment?aid=1234',
+ perms=permissions.READ_ONLY_PERMISSIONSET)
+ self.issue.labels.append('Restrict-View-Nobody')
+ self.assertRaises( # Issue is restricted
+ permissions.PermissionException,
+ self.servlet.GatherPageData, mr)
+
+ def testGatherPageData_NoSuchAttachment(self):
+ _request, mr = testing_helpers.GetRequestObjects(
+ project=self.project,
+ path='/p/proj/issues/attachmentText?aid=9999',
+ perms=permissions.READ_ONLY_PERMISSIONSET)
+ with self.assertRaises(webapp2.HTTPException) as cm:
+ self.servlet.GatherPageData(mr)
+ self.assertEqual(404, cm.exception.code)
+
+ def testGatherPageData_AttachmentDeleted(self):
+ """If the attachment was deleted, give a 404."""
+ _request, mr = testing_helpers.GetRequestObjects(
+ project=self.project,
+ path='/p/proj/issues/attachmentText?aid=1234',
+ perms=permissions.READ_ONLY_PERMISSIONSET)
+ self.attach1.deleted = True
+ with self.assertRaises(webapp2.HTTPException) as cm:
+ self.servlet.GatherPageData(mr)
+ self.assertEqual(404, cm.exception.code)
+
+ def testGatherPageData_Normal(self):
+ _request, mr = testing_helpers.GetRequestObjects(
+ project=self.project,
+ path='/p/proj/issues/attachmentText?id=1&aid=1234',
+ perms=permissions.READ_ONLY_PERMISSIONSET)
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(1, page_data['local_id'])
+ self.assertEqual('a.txt', page_data['filename'])
+ self.assertEqual('43 bytes', page_data['filesize'])
+ self.assertEqual(ezt.boolean(False), page_data['should_prettify'])
+ self.assertEqual(ezt.boolean(False), page_data['is_binary'])
+ self.assertEqual(ezt.boolean(False), page_data['too_large'])
+
+ file_lines = page_data['file_lines']
+ self.assertEqual(1, len(file_lines))
+ self.assertEqual(1, file_lines[0].num)
+ self.assertEqual('/app_default_bucket/pid/attachments/abcdefg',
+ file_lines[0].line)
+
+ self.assertEqual(None, page_data['code_reviews'])
+
+ @patch('framework.filecontent.DecodeFileContents')
+ def testGatherPageData_HugeFile(self, mock_DecodeFileContents):
+ _request, mr = testing_helpers.GetRequestObjects(
+ project=self.project,
+ path='/p/proj/issues/attachmentText?id=1&aid=1234',
+ perms=permissions.READ_ONLY_PERMISSIONSET)
+ mock_DecodeFileContents.return_value = (
+ 'too large text', False, True)
+
+ page_data = self.servlet.GatherPageData(mr)
+
+ self.assertEqual(ezt.boolean(False), page_data['should_prettify'])
+ self.assertEqual(ezt.boolean(False), page_data['is_binary'])
+ self.assertEqual(ezt.boolean(True), page_data['too_large'])
diff --git a/tracker/test/issuebulkedit_test.py b/tracker/test/issuebulkedit_test.py
new file mode 100644
index 0000000..89d9bc3
--- /dev/null
+++ b/tracker/test/issuebulkedit_test.py
@@ -0,0 +1,892 @@
+# Copyright 2016 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
+
+"""Unittests for monorail.tracker.issuebulkedit."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mock
+import os
+import unittest
+import webapp2
+
+from google.appengine.api import memcache
+from google.appengine.ext import testbed
+
+from framework import exceptions
+from framework import permissions
+from proto import tracker_pb2
+from services import service_manager
+from services import tracker_fulltext
+from testing import fake
+from testing import testing_helpers
+from tracker import issuebulkedit
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+
+
+class Response(object):
+
+ def __init__(self):
+ self.status = None
+
+
+class IssueBulkEditTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ features=fake.FeaturesService(),
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ issue_star=fake.IssueStarService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+ self.servlet = issuebulkedit.IssueBulkEdit(
+ 'req', 'res', services=self.services)
+ self.mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET)
+ self.project = self.services.project.TestAddProject(
+ name='proj', project_id=789, owner_ids=[111])
+ self.cnxn = 'fake connection'
+ self.config = self.services.config.GetProjectConfig(
+ self.cnxn, self.project.project_id)
+ self.services.config.StoreConfig(self.cnxn, self.config)
+ self.owner = self.services.user.TestAddUser('owner@example.com', 111)
+
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_datastore_v3_stub()
+
+ self.mocked_methods = {}
+
+ def tearDown(self):
+ """Restore mocked objects of other modules."""
+ self.testbed.deactivate()
+ for obj, items in self.mocked_methods.items():
+ for member, previous_value in items.items():
+ setattr(obj, member, previous_value)
+
+ def testAssertBasePermission(self):
+ """Permit users with EDIT_ISSUE and ADD_ISSUE_COMMENT permissions."""
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET)
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ self.servlet.AssertBasePermission(self.mr)
+
+ def testGatherPageData(self):
+ """Test GPD works in a normal no-corner-cases case."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 0, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project)
+ mr.local_id_list = [local_id_1]
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(1, page_data['num_issues'])
+
+ def testGatherPageData_CustomFieldEdition(self):
+ """Test GPD works in a normal no-corner-cases case."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 0, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project, perms=permissions.PermissionSet([]))
+ mr.local_id_list = [local_id_1]
+ mr.auth.effective_ids = {222}
+
+ fd_not_restricted = tracker_bizobj.MakeFieldDef(
+ 123,
+ 789,
+ 'CPU',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=False)
+ self.config.field_defs.append(fd_not_restricted)
+
+ fd_restricted = tracker_bizobj.MakeFieldDef(
+ 124,
+ 789,
+ 'CPU',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=True)
+ self.config.field_defs.append(fd_restricted)
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertTrue(page_data['fields'][0].is_editable)
+ self.assertFalse(page_data['fields'][1].is_editable)
+
+ def testGatherPageData_NoIssues(self):
+ """Test GPD when no issues are specified in the mr."""
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project)
+ self.assertRaises(exceptions.InputException,
+ self.servlet.GatherPageData, mr)
+
+ def testGatherPageData_FilteredIssues(self):
+ """Test GPD when all specified issues get filtered out."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789,
+ 1,
+ 'issue summary',
+ 'New',
+ 0,
+ reporter_id=111,
+ labels=['restrict-view-Googler'])
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project)
+ mr.local_id_list = [local_id_1]
+
+ self.assertRaises(webapp2.HTTPException,
+ self.servlet.GatherPageData, mr)
+
+ def testGatherPageData_TypeLabels(self):
+ """Test that GPD displays a custom field for appropriate issues."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789,
+ 1,
+ 'issue summary',
+ 'New',
+ 0,
+ reporter_id=111,
+ labels=['type-customlabels'])
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project)
+ mr.local_id_list = [local_id_1]
+
+ fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.config.field_defs.append(fd)
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(1, len(page_data['fields']))
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testProcessFormData(self, _create_task_mock):
+ """Test that PFD works in a normal no-corner-cases case."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.local_id_list = [local_id_1]
+
+ post_data = fake.PostData(
+ owner=['owner@example.com'], can=[1],
+ q=[''], colspec=[''], sort=[''], groupby=[''], start=[0], num=[100])
+ self._MockMethods()
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.assertTrue('list?can=1&q=&saved=1' in url)
+
+ def testProcessFormData_NoIssues(self):
+ """Test PFD when no issues are specified."""
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ post_data = fake.PostData()
+ self.servlet.response = Response()
+ self.servlet.ProcessFormData(mr, post_data)
+ # 400 == bad request
+ self.assertEqual(400, self.servlet.response.status)
+
+ def testProcessFormData_NoUser(self):
+ """Test PFD when the user is not logged in."""
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project)
+ mr.local_id_list = [99999]
+ post_data = fake.PostData()
+ self.servlet.response = Response()
+ self.servlet.ProcessFormData(mr, post_data)
+ # 400 == bad request
+ self.assertEqual(400, self.servlet.response.status)
+
+ def testProcessFormData_CantComment(self):
+ """Test PFD when the user can't comment on any of the issues."""
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.EMPTY_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.local_id_list = [99999]
+ post_data = fake.PostData()
+ self.servlet.response = Response()
+ self.servlet.ProcessFormData(mr, post_data)
+ # 400 == bad request
+ self.assertEqual(400, self.servlet.response.status)
+
+ def testProcessFormData_CantEdit(self):
+ """Test PFD when the user can't edit any issue metadata."""
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.local_id_list = [99999]
+ post_data = fake.PostData()
+ self.servlet.response = Response()
+ self.servlet.ProcessFormData(mr, post_data)
+ # 400 == bad request
+ self.assertEqual(400, self.servlet.response.status)
+
+ def testProcessFormData_CantMove(self):
+ """Test PFD when the user can't move issues."""
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.COMMITTER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.local_id_list = [99999]
+ post_data = fake.PostData(move_to=['proj'])
+ self.servlet.response = Response()
+ self.servlet.ProcessFormData(mr, post_data)
+ # 400 == bad request
+ self.assertEqual(400, self.servlet.response.status)
+
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ mr.perms = permissions.OWNER_ACTIVE_PERMISSIONSET
+ mr.local_id_list = [local_id_1]
+ mr.project_name = 'proj'
+ self._MockMethods()
+ self.servlet.ProcessFormData(mr, post_data)
+ self.assertEqual(
+ 'The issues are already in project proj', mr.errors.move_to)
+
+ post_data = fake.PostData(move_to=['notexist'])
+ self.servlet.ProcessFormData(mr, post_data)
+ self.assertEqual('No such project: notexist', mr.errors.move_to)
+
+ def _MockMethods(self):
+ # Mock methods of other modules to avoid unnecessary testing
+ self.mocked_methods[tracker_fulltext] = {
+ 'IndexIssues': tracker_fulltext.IndexIssues,
+ 'UnindexIssues': tracker_fulltext.UnindexIssues}
+ def DoNothing(*_args, **_kwargs):
+ pass
+ self.servlet.PleaseCorrect = DoNothing
+ tracker_fulltext.IndexIssues = DoNothing
+ tracker_fulltext.UnindexIssues = DoNothing
+
+ def GetFirstAmendment(self, project_id, local_id):
+ issue = self.services.issue.GetIssueByLocalID(
+ self.cnxn, project_id, local_id)
+ issue_id = issue.issue_id
+ comments = self.services.issue.GetCommentsForIssue(self.cnxn, issue_id)
+ last_comment = comments[-1]
+ first_amendment = last_comment.amendments[0]
+ return first_amendment.field, first_amendment.newvalue
+
+ def testProcessFormData_BadUserField(self):
+ """Test PFD when a nonexistent user is added as a field value."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.local_id_list = [local_id_1]
+
+ fd = tracker_bizobj.MakeFieldDef(
+ 12345, 789, 'PM', tracker_pb2.FieldTypes.USER_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.config.field_defs.append(fd)
+
+ post_data = fake.PostData(
+ custom_12345=['ghost@gmail.com'], owner=['owner@example.com'], can=[1],
+ q=[''], colspec=[''], sort=[''], groupby=[''], start=[0], num=[100])
+ self._MockMethods()
+ self.servlet.ProcessFormData(mr, post_data)
+ self.assertEqual('User not found.', mr.errors.custom_fields[0].message)
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testProcessFormData_CustomFields(self, _create_task_mock):
+ """Test PFD processes edits to custom fields."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.local_id_list = [local_id_1]
+
+ fd = tracker_bizobj.MakeFieldDef(
+ 12345, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.config.field_defs.append(fd)
+
+ post_data = fake.PostData(
+ custom_12345=['10'],
+ owner=['owner@example.com'],
+ can=[1],
+ q=[''],
+ colspec=[''],
+ sort=[''],
+ groupby=[''],
+ start=[0],
+ num=[100])
+ self._MockMethods()
+ self.servlet.ProcessFormData(mr, post_data)
+ self.assertEqual(
+ (tracker_pb2.FieldID.CUSTOM, '10'),
+ self.GetFirstAmendment(789, local_id_1))
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testProcessFormData_RestrictedCustomFieldsAccept(self, _create_task_mock):
+ """We accept edits to restricted fields by editors (or admins)."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.PermissionSet(
+ [
+ permissions.EDIT_ISSUE, permissions.ADD_ISSUE_COMMENT,
+ permissions.VIEW
+ ]),
+ user_info={'user_id': 111})
+ mr.local_id_list = [local_id_1]
+
+ fd = tracker_bizobj.MakeFieldDef(
+ 12345,
+ 789,
+ 'CPU',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=True)
+ fd.editor_ids = [111]
+ self.config.field_defs.append(fd)
+
+ post_data = fake.PostData(
+ custom_12345=['10'],
+ owner=['owner@example.com'],
+ can=[1],
+ q=[''],
+ colspec=[''],
+ sort=[''],
+ groupby=[''],
+ start=[0],
+ num=[100])
+ self._MockMethods()
+ self.servlet.ProcessFormData(mr, post_data)
+ self.assertEqual(
+ (tracker_pb2.FieldID.CUSTOM, '10'),
+ self.GetFirstAmendment(789, local_id_1))
+
+ def testProcessFormData_RestrictedCustomFieldsReject(self):
+ """We reject edits to restricted fields by non-editors (and non-admins)."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.PermissionSet(
+ [
+ permissions.EDIT_ISSUE, permissions.ADD_ISSUE_COMMENT,
+ permissions.VIEW
+ ]),
+ user_info={'user_id': 111})
+ mr.local_id_list = [local_id_1]
+
+ fd_int = tracker_bizobj.MakeFieldDef(
+ 11111,
+ 789,
+ 'fd_int',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=True)
+ fd_enum = tracker_bizobj.MakeFieldDef(
+ 44444,
+ 789,
+ 'fdEnum',
+ tracker_pb2.FieldTypes.ENUM_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=True)
+ fd_int.admin_ids = [222]
+ fd_enum.editor_ids = [333]
+ self.config.field_defs = [fd_int, fd_enum]
+
+ post_data_add_fv = fake.PostData(
+ custom_11111=['10'],
+ owner=['owner@example.com'],
+ can=[1],
+ q=[''],
+ colspec=[''],
+ sort=[''],
+ groupby=[''],
+ start=[0],
+ num=[100])
+ post_data_rm_fv = fake.PostData(
+ op_custom_11111=['remove'],
+ custom_11111=['10'],
+ owner=['owner@example.com'],
+ can=[1],
+ q=[''],
+ colspec=[''],
+ sort=[''],
+ groupby=[''],
+ start=[0],
+ num=[100])
+ post_data_clear_fd = fake.PostData(
+ op_custom_11111=['clear'],
+ owner=['owner@example.com'],
+ can=[1],
+ q=[''],
+ colspec=[''],
+ sort=[''],
+ groupby=[''],
+ start=[0],
+ num=[100])
+ post_data_label_edits_enum = fake.PostData(
+ label=['fdEnum-a'],
+ owner=['owner@example.com'],
+ can=[1],
+ q=[''],
+ colspec=[''],
+ sort=[''],
+ groupby=[''],
+ start=[0],
+ num=[100])
+ post_data_label_rm_enum = fake.PostData(
+ label=['-fdEnum-b'],
+ owner=['owner@example.com'],
+ can=[1],
+ q=[''],
+ colspec=[''],
+ sort=[''],
+ groupby=[''],
+ start=[0],
+ num=[100])
+
+ self._MockMethods()
+ self.assertRaises(
+ AssertionError, self.servlet.ProcessFormData, mr, post_data_add_fv)
+ self.assertRaises(
+ AssertionError, self.servlet.ProcessFormData, mr, post_data_rm_fv)
+ self.assertRaises(
+ AssertionError, self.servlet.ProcessFormData, mr, post_data_clear_fd)
+ self.assertRaises(
+ AssertionError, self.servlet.ProcessFormData, mr,
+ post_data_label_edits_enum)
+ self.assertRaises(
+ AssertionError, self.servlet.ProcessFormData, mr,
+ post_data_label_rm_enum)
+
+ def testProcessFormData_DuplicateStatus_MergeSameIssue(self):
+ """Test PFD processes null/cleared status values."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+
+ created_issue_2 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 112, reporter_id=112)
+ self.services.issue.TestAddIssue(created_issue_2)
+ merge_into_local_id_2 = created_issue_2.local_id
+
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.local_id_list = [local_id_1, merge_into_local_id_2]
+ mr.project_name = 'proj'
+
+ # Add required project_name to merge_into_issue.
+ merge_into_issue = self.services.issue.GetIssueByLocalID(
+ mr.cnxn, self.project.project_id, merge_into_local_id_2)
+ merge_into_issue.project_name = 'proj'
+
+ post_data = fake.PostData(status=['Duplicate'],
+ merge_into=[str(merge_into_local_id_2)], owner=['owner@example.com'],
+ can=[1], q=[''], colspec=[''], sort=[''], groupby=[''], start=[0],
+ num=[100])
+ self._MockMethods()
+ self.servlet.ProcessFormData(mr, post_data)
+ self.assertEqual('Cannot merge issue into itself', mr.errors.merge_into_id)
+
+ def testProcessFormData_DuplicateStatus_MergeMissingIssue(self):
+ """Test PFD processes null/cleared status values."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ created_issue_2 = fake.MakeTestIssue(
+ 789, 1, 'issue summary2', 'New', 112, reporter_id=112)
+ self.services.issue.TestAddIssue(created_issue_2)
+ local_id_2 = created_issue_2.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.local_id_list = [local_id_1, local_id_2]
+ mr.project_name = 'proj'
+
+ post_data = fake.PostData(status=['Duplicate'],
+ merge_into=['non existant id'], owner=['owner@example.com'],
+ can=[1], q=[''], colspec=[''], sort=[''], groupby=[''], start=[0],
+ num=[100])
+ self._MockMethods()
+ self.servlet.ProcessFormData(mr, post_data)
+ self.assertEqual('Please enter an issue ID', mr.errors.merge_into_id)
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testProcessFormData_DuplicateStatus_Success(self, _create_task_mock):
+ """Test PFD processes null/cleared status values."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ created_issue_2 = fake.MakeTestIssue(
+ 789, 2, 'issue summary2', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_2)
+ local_id_2 = created_issue_2.local_id
+ created_issue_3 = fake.MakeTestIssue(
+ 789, 3, 'issue summary3', 'New', 112, reporter_id=112)
+ self.services.issue.TestAddIssue(created_issue_3)
+ merge_into_local_id_3 = created_issue_3.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.local_id_list = [local_id_1, local_id_2]
+ mr.project_name = 'proj'
+
+ post_data = fake.PostData(status=['Duplicate'],
+ merge_into=[str(merge_into_local_id_3)], owner=['owner@example.com'],
+ can=[1], q=[''], colspec=[''], sort=[''], groupby=[''], start=[0],
+ num=[100])
+ self._MockMethods()
+
+ # Add project_name, CCs and starrers to the merge_into_issue.
+ merge_into_issue = self.services.issue.GetIssueByLocalID(
+ mr.cnxn, self.project.project_id, merge_into_local_id_3)
+ merge_into_issue.project_name = 'proj'
+ merge_into_issue.cc_ids = [113, 120]
+ self.services.issue_star.SetStar(
+ mr.cnxn, self.services, None, merge_into_issue.issue_id, 120, True)
+
+ # Add project_name, CCs and starrers to the source issues.
+ # Issue 1
+ issue_1 = self.services.issue.GetIssueByLocalID(
+ mr.cnxn, self.project.project_id, local_id_1)
+ issue_1.project_name = 'proj'
+ issue_1.cc_ids = [113, 114]
+ self.services.issue_star.SetStar(
+ mr.cnxn, self.services, None, issue_1.issue_id, 113, True)
+ # Issue 2
+ issue_2 = self.services.issue.GetIssueByLocalID(
+ mr.cnxn, self.project.project_id, local_id_2)
+ issue_2.project_name = 'proj'
+ issue_2.cc_ids = [113, 115, 118]
+ self.services.issue_star.SetStar(
+ mr.cnxn, self.services, None, issue_2.issue_id, 114, True)
+ self.services.issue_star.SetStar(
+ mr.cnxn, self.services, None, issue_2.issue_id, 115, True)
+
+ self.servlet.ProcessFormData(mr, post_data)
+
+ # Verify both source issues were updated.
+ self.assertEqual(
+ (tracker_pb2.FieldID.STATUS, 'Duplicate'),
+ self.GetFirstAmendment(self.project.project_id, local_id_1))
+ self.assertEqual(
+ (tracker_pb2.FieldID.STATUS, 'Duplicate'),
+ self.GetFirstAmendment(self.project.project_id, local_id_2))
+
+ # Verify that the merge into issue was updated with a comment.
+ comments = self.services.issue.GetCommentsForIssue(
+ self.cnxn, merge_into_issue.issue_id)
+ self.assertEqual(
+ 'Issue 1 has been merged into this issue.\n'
+ 'Issue 2 has been merged into this issue.', comments[-1].content)
+
+ # Verify CC lists and owner were merged to the merge_into issue.
+ self.assertEqual(
+ [113, 120, 114, 115, 118, 111], merge_into_issue.cc_ids)
+ # Verify new starrers were added to the merge_into issue.
+ self.assertEqual(4,
+ self.services.issue_star.CountItemStars(
+ self.cnxn, merge_into_issue.issue_id))
+ self.assertEqual([120, 113, 114, 115],
+ self.services.issue_star.LookupItemStarrers(
+ self.cnxn, merge_into_issue.issue_id))
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testProcessFormData_ClearStatus(self, _create_task_mock):
+ """Test PFD processes null/cleared status values."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.local_id_list = [local_id_1]
+
+ post_data = fake.PostData(
+ op_statusenter=['clear'], owner=['owner@example.com'], can=[1],
+ q=[''], colspec=[''], sort=[''], groupby=[''], start=[0], num=[100])
+ self._MockMethods()
+ self.servlet.ProcessFormData(mr, post_data)
+ self.assertEqual(
+ (tracker_pb2.FieldID.STATUS, ''), self.GetFirstAmendment(
+ 789, local_id_1))
+
+ def testProcessFormData_InvalidOwner(self):
+ """Test PFD rejects invalid owner emails."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 0, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.local_id_list = [local_id_1]
+ post_data = fake.PostData(
+ owner=['invalid'])
+ self.servlet.response = Response()
+ self._MockMethods()
+ self.servlet.ProcessFormData(mr, post_data)
+ self.assertTrue(mr.errors.AnyErrors())
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testProcessFormData_MoveTo(self, _create_task_mock):
+ """Test PFD processes move_to values."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ move_to_project = self.services.project.TestAddProject(
+ name='proj2', project_id=790, owner_ids=[111])
+
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.project_name = 'proj'
+ mr.local_id_list = [local_id_1]
+
+ self._MockMethods()
+ post_data = fake.PostData(
+ move_to=['proj2'], can=[1], q=[''],
+ colspec=[''], sort=[''], groupby=[''], start=[0], num=[100])
+ self.servlet.response = Response()
+ self.servlet.ProcessFormData(mr, post_data)
+
+ issue = self.services.issue.GetIssueByLocalID(
+ self.cnxn, move_to_project.project_id, local_id_1)
+ self.assertIsNotNone(issue)
+
+ def testProcessFormData_InvalidBlockIssues(self):
+ """Test PFD processes invalid blocked_on and blocking values."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.project_name = 'proj'
+ mr.local_id_list = [local_id_1]
+
+ self._MockMethods()
+ post_data = fake.PostData(
+ op_blockedonenter=['append'], blocked_on=['12345'],
+ op_blockingenter=['append'], blocking=['54321'],
+ can=[1], q=[''],
+ colspec=[''], sort=[''], groupby=[''], start=[0], num=[100])
+ self.servlet.ProcessFormData(mr, post_data)
+
+ self.assertEqual('Invalid issue ID 12345', mr.errors.blocked_on)
+ self.assertEqual('Invalid issue ID 54321', mr.errors.blocking)
+
+ def testProcessFormData_BlockIssuesOnItself(self):
+ """Test PFD processes invalid blocked_on and blocking values."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+ created_issue_2 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_2)
+ local_id_2 = created_issue_2.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.project_name = 'proj'
+ mr.local_id_list = [local_id_1, local_id_2]
+
+ self._MockMethods()
+ post_data = fake.PostData(
+ op_blockedonenter=['append'], blocked_on=[str(local_id_1)],
+ op_blockingenter=['append'], blocking=[str(local_id_2)],
+ can=[1], q=[''],
+ colspec=[''], sort=[''], groupby=[''], start=[0], num=[100])
+ self.servlet.ProcessFormData(mr, post_data)
+
+ self.assertEqual('Cannot block an issue on itself.', mr.errors.blocked_on)
+ self.assertEqual('Cannot block an issue on itself.', mr.errors.blocking)
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testProcessFormData_NormalBlockIssues(self, _create_task_mock):
+ """Test PFD processes blocked_on and blocking values."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+
+ created_issueid = fake.MakeTestIssue(
+ 789, 2, 'blocking', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issueid)
+ blocking_id = created_issueid.local_id
+
+ created_issueid = fake.MakeTestIssue(
+ 789, 3, 'blocked on', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issueid)
+ blocked_on_id = created_issueid.local_id
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.project_name = 'proj'
+ mr.local_id_list = [local_id_1]
+
+ self._MockMethods()
+ post_data = fake.PostData(
+ op_blockedonenter=['append'], blocked_on=[str(blocked_on_id)],
+ op_blockingenter=['append'], blocking=[str(blocking_id)],
+ can=[1], q=[''],
+ colspec=[''], sort=[''], groupby=[''], start=[0], num=[100])
+ self.servlet.ProcessFormData(mr, post_data)
+
+ self.assertIsNone(mr.errors.blocked_on)
+ self.assertIsNone(mr.errors.blocking)
+
+ def testProcessFormData_TooLongComment(self):
+ """Test PFD rejects comments that are too long."""
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, 'issue summary', 'New', 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ local_id_1 = created_issue_1.local_id
+
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project,
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+ user_info={'user_id': 111})
+ mr.local_id_list = [local_id_1]
+
+ post_data = fake.PostData(
+ owner=['owner@example.com'],
+ can=[1],
+ q=[''],
+ colspec=[''],
+ sort=[''],
+ groupby=[''],
+ start=[0],
+ num=[100],
+ comment=[' ' + 'c' * tracker_constants.MAX_COMMENT_CHARS + ' '])
+ self._MockMethods()
+ self.servlet.ProcessFormData(mr, post_data)
+ self.assertTrue(mr.errors.AnyErrors())
+ self.assertEqual('Comment is too long.', mr.errors.comment)
diff --git a/tracker/test/issuedetailezt_test.py b/tracker/test/issuedetailezt_test.py
new file mode 100644
index 0000000..d3b8327
--- /dev/null
+++ b/tracker/test/issuedetailezt_test.py
@@ -0,0 +1,306 @@
+# Copyright 2016 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
+
+"""Unittests for monorail.tracker.issuedetailezt."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import mock
+import mox
+import time
+import unittest
+
+import settings
+from businesslogic import work_env
+from proto import features_pb2
+from features import hotlist_views
+from features import send_notifications
+from framework import authdata
+from framework import exceptions
+from framework import framework_views
+from framework import framework_helpers
+from framework import urls
+from framework import permissions
+from framework import profiler
+from framework import sorting
+from framework import template_helpers
+from proto import project_pb2
+from proto import tracker_pb2
+from proto import user_pb2
+from services import service_manager
+from services import issue_svc
+from services import tracker_fulltext
+from testing import fake
+from testing import testing_helpers
+from tracker import issuedetailezt
+from tracker import tracker_constants
+from tracker import tracker_helpers
+
+
+class GetAdjacentIssueTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ project=fake.ProjectService(),
+ issue_star=fake.IssueStarService(),
+ spam=fake.SpamService())
+ self.services.project.TestAddProject('proj', project_id=789)
+ self.mr = testing_helpers.MakeMonorailRequest()
+ self.mr.auth.user_id = 111
+ self.mr.auth.effective_ids = {111}
+ self.mr.me_user_id = 111
+ self.work_env = work_env.WorkEnv(
+ self.mr, self.services, 'Testing phase')
+
+ def testGetAdjacentIssue_PrevIssue(self):
+ cur_issue = fake.MakeTestIssue(789, 2, 'sum', 'New', 111, issue_id=78902)
+ next_issue = fake.MakeTestIssue(789, 3, 'sum', 'New', 111, issue_id=78903)
+ prev_issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
+ self.services.issue.TestAddIssue(cur_issue)
+ self.services.issue.TestAddIssue(next_issue)
+ self.services.issue.TestAddIssue(prev_issue)
+
+ with self.work_env as we:
+ we.FindIssuePositionInSearch = mock.Mock(
+ return_value=[78901, 1, 78903, 3])
+
+ actual_issue = issuedetailezt.GetAdjacentIssue(
+ self.mr, we, cur_issue)
+ self.assertEqual(prev_issue, actual_issue)
+ we.FindIssuePositionInSearch.assert_called_once_with(cur_issue)
+
+ def testGetAdjacentIssue_NextIssue(self):
+ cur_issue = fake.MakeTestIssue(789, 2, 'sum', 'New', 111, issue_id=78902)
+ next_issue = fake.MakeTestIssue(789, 3, 'sum', 'New', 111, issue_id=78903)
+ prev_issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
+ self.services.issue.TestAddIssue(cur_issue)
+ self.services.issue.TestAddIssue(next_issue)
+ self.services.issue.TestAddIssue(prev_issue)
+
+ with self.work_env as we:
+ we.FindIssuePositionInSearch = mock.Mock(
+ return_value=[78901, 1, 78903, 3])
+
+ actual_issue = issuedetailezt.GetAdjacentIssue(
+ self.mr, we, cur_issue, next_issue=True)
+ self.assertEqual(next_issue, actual_issue)
+ we.FindIssuePositionInSearch.assert_called_once_with(cur_issue)
+
+ def testGetAdjacentIssue_NotFound(self):
+ cur_issue = fake.MakeTestIssue(789, 2, 'sum', 'New', 111, issue_id=78902)
+ prev_issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
+ self.services.issue.TestAddIssue(cur_issue)
+ self.services.issue.TestAddIssue(prev_issue)
+
+ with self.work_env as we:
+ we.FindIssuePositionInSearch = mock.Mock(
+ return_value=[78901, 1, 78903, 3])
+
+ with self.assertRaises(exceptions.NoSuchIssueException):
+ issuedetailezt.GetAdjacentIssue(
+ self.mr, we, cur_issue, next_issue=True)
+ we.FindIssuePositionInSearch.assert_called_once_with(cur_issue)
+
+ def testGetAdjacentIssue_Hotlist(self):
+ cur_issue = fake.MakeTestIssue(789, 2, 'sum', 'New', 111, issue_id=78902)
+ next_issue = fake.MakeTestIssue(789, 3, 'sum', 'New', 111, issue_id=78903)
+ prev_issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
+ self.services.issue.TestAddIssue(cur_issue)
+ self.services.issue.TestAddIssue(next_issue)
+ self.services.issue.TestAddIssue(prev_issue)
+ hotlist = fake.Hotlist('name', 678, owner_ids=[111])
+
+ with self.work_env as we:
+ we.GetIssuePositionInHotlist = mock.Mock(
+ return_value=[78901, 1, 78903, 3])
+
+ actual_issue = issuedetailezt.GetAdjacentIssue(
+ self.mr, we, cur_issue, hotlist=hotlist, next_issue=True)
+ self.assertEqual(next_issue, actual_issue)
+ we.GetIssuePositionInHotlist.assert_called_once_with(
+ cur_issue, hotlist, self.mr.can, self.mr.sort_spec,
+ self.mr.group_by_spec)
+
+
+class FlipperRedirectTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ features=fake.FeaturesService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ project=fake.ProjectService())
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=987, committer_ids=[111])
+ self.next_servlet = issuedetailezt.FlipperNext(
+ 'req', 'res', services=self.services)
+ self.prev_servlet = issuedetailezt.FlipperPrev(
+ 'req', 'res', services=self.services)
+ self.list_servlet = issuedetailezt.FlipperList(
+ 'req', 'res', services=self.services)
+ mr = testing_helpers.MakeMonorailRequest(project=self.project)
+ mr.local_id = 123
+ mr.me_user_id = 111
+
+ self.next_servlet.mr = mr
+ self.prev_servlet.mr = mr
+ self.list_servlet.mr = mr
+
+ self.fake_issue_1 = fake.MakeTestIssue(987, 123, 'summary', 'New', 111,
+ project_name='rutabaga')
+ self.services.issue.TestAddIssue(self.fake_issue_1)
+ self.fake_issue_2 = fake.MakeTestIssue(987, 456, 'summary', 'New', 111,
+ project_name='rutabaga')
+ self.services.issue.TestAddIssue(self.fake_issue_2)
+ self.fake_issue_3 = fake.MakeTestIssue(987, 789, 'summary', 'New', 111,
+ project_name='potato')
+ self.services.issue.TestAddIssue(self.fake_issue_3)
+
+ self.next_servlet.redirect = mock.Mock()
+ self.prev_servlet.redirect = mock.Mock()
+ self.list_servlet.redirect = mock.Mock()
+
+ @mock.patch('tracker.issuedetailezt.GetAdjacentIssue')
+ def testFlipperNext(self, patchGetAdjacentIssue):
+ patchGetAdjacentIssue.return_value = self.fake_issue_2
+ self.next_servlet.mr.GetIntParam = mock.Mock(return_value=None)
+
+ self.next_servlet.get(project_name='proj', viewed_username=None)
+ self.next_servlet.mr.GetIntParam.assert_called_once_with('hotlist_id')
+ patchGetAdjacentIssue.assert_called_once()
+ self.next_servlet.redirect.assert_called_once_with(
+ '/p/rutabaga/issues/detail?id=456')
+
+ @mock.patch('tracker.issuedetailezt.GetAdjacentIssue')
+ def testFlipperNext_Hotlist(self, patchGetAdjacentIssue):
+ patchGetAdjacentIssue.return_value = self.fake_issue_3
+ self.next_servlet.mr.GetIntParam = mock.Mock(return_value=123)
+ # TODO(jeffcarp): Mock hotlist_id param on path here.
+
+ self.next_servlet.get(project_name='proj', viewed_username=None)
+ self.next_servlet.mr.GetIntParam.assert_called_with('hotlist_id')
+ self.next_servlet.redirect.assert_called_once_with(
+ '/p/potato/issues/detail?id=789')
+
+ @mock.patch('tracker.issuedetailezt.GetAdjacentIssue')
+ def testFlipperPrev(self, patchGetAdjacentIssue):
+ patchGetAdjacentIssue.return_value = self.fake_issue_2
+ self.next_servlet.mr.GetIntParam = mock.Mock(return_value=None)
+
+ self.prev_servlet.get(project_name='proj', viewed_username=None)
+ self.prev_servlet.mr.GetIntParam.assert_called_with('hotlist_id')
+ patchGetAdjacentIssue.assert_called_once()
+ self.prev_servlet.redirect.assert_called_once_with(
+ '/p/rutabaga/issues/detail?id=456')
+
+ @mock.patch('tracker.issuedetailezt.GetAdjacentIssue')
+ def testFlipperPrev_Hotlist(self, patchGetAdjacentIssue):
+ patchGetAdjacentIssue.return_value = self.fake_issue_3
+ self.prev_servlet.mr.GetIntParam = mock.Mock(return_value=123)
+ # TODO(jeffcarp): Mock hotlist_id param on path here.
+
+ self.prev_servlet.get(project_name='proj', viewed_username=None)
+ self.prev_servlet.mr.GetIntParam.assert_called_with('hotlist_id')
+ self.prev_servlet.redirect.assert_called_once_with(
+ '/p/potato/issues/detail?id=789')
+
+ @mock.patch('tracker.issuedetailezt._ComputeBackToListURL')
+ def testFlipperList(self, patch_ComputeBackToListURL):
+ patch_ComputeBackToListURL.return_value = '/p/test/issues/list'
+ self.list_servlet.mr.GetIntParam = mock.Mock(return_value=None)
+
+ self.list_servlet.get()
+
+ self.list_servlet.mr.GetIntParam.assert_called_with('hotlist_id')
+ patch_ComputeBackToListURL.assert_called_once()
+ self.list_servlet.redirect.assert_called_once_with(
+ '/p/test/issues/list')
+
+ @mock.patch('tracker.issuedetailezt._ComputeBackToListURL')
+ def testFlipperList_Hotlist(self, patch_ComputeBackToListURL):
+ patch_ComputeBackToListURL.return_value = '/p/test/issues/list'
+ self.list_servlet.mr.GetIntParam = mock.Mock(return_value=123)
+
+ self.list_servlet.get()
+
+ self.list_servlet.mr.GetIntParam.assert_called_with('hotlist_id')
+ self.list_servlet.redirect.assert_called_once_with(
+ '/p/test/issues/list')
+
+
+class ShouldShowFlipperTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+
+ def VerifyShouldShowFlipper(
+ self, expected, query, sort_spec, can, create_issues=0):
+ """Instantiate a _Flipper and check if makes a pipeline or not."""
+ services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ project=fake.ProjectService(),
+ user=fake.UserService())
+ project = services.project.TestAddProject(
+ 'proj', project_id=987, committer_ids=[111])
+ mr = testing_helpers.MakeMonorailRequest(project=project)
+ mr.query = query
+ mr.sort_spec = sort_spec
+ mr.can = can
+ mr.project_name = project.project_name
+ mr.project = project
+
+ for idx in range(create_issues):
+ _created_issue = fake.MakeTestIssue(
+ project.project_id,
+ idx,
+ 'summary_%d' % idx,
+ 'status',
+ 111,
+ reporter_id=111)
+ services.issue.TestAddIssue(_created_issue)
+
+ self.assertEqual(expected, issuedetailezt._ShouldShowFlipper(mr, services))
+
+ def testShouldShowFlipper_RegularSizedProject(self):
+ # If the user is looking for a specific issue, no flipper.
+ self.VerifyShouldShowFlipper(
+ False, '123', '', tracker_constants.OPEN_ISSUES_CAN)
+ self.VerifyShouldShowFlipper(False, '123', '', 5)
+ self.VerifyShouldShowFlipper(
+ False, '123', 'priority', tracker_constants.OPEN_ISSUES_CAN)
+
+ # If the user did a search or sort or all in a small can, show flipper.
+ self.VerifyShouldShowFlipper(
+ True, 'memory leak', '', tracker_constants.OPEN_ISSUES_CAN)
+ self.VerifyShouldShowFlipper(
+ True, 'id=1,2,3', '', tracker_constants.OPEN_ISSUES_CAN)
+ # Any can other than 1 or 2 is doing a query and so it should have a
+ # failry narrow result set size. 5 is issues starred by me.
+ self.VerifyShouldShowFlipper(True, '', '', 5)
+ self.VerifyShouldShowFlipper(
+ True, '', 'status', tracker_constants.OPEN_ISSUES_CAN)
+
+ # In a project without a huge number of issues, still show the flipper even
+ # if there was no specific query.
+ self.VerifyShouldShowFlipper(
+ True, '', '', tracker_constants.OPEN_ISSUES_CAN)
+
+ def testShouldShowFlipper_LargeSizedProject(self):
+ settings.threshold_to_suppress_prev_next = 1
+
+ # In a project that has tons of issues, save time by not showing the
+ # flipper unless there was a specific query, sort, or can.
+ self.VerifyShouldShowFlipper(
+ False, '', '', tracker_constants.ALL_ISSUES_CAN, create_issues=3)
+ self.VerifyShouldShowFlipper(
+ False, '', '', tracker_constants.OPEN_ISSUES_CAN, create_issues=3)
diff --git a/tracker/test/issueentry_test.py b/tracker/test/issueentry_test.py
new file mode 100644
index 0000000..4a64d7c
--- /dev/null
+++ b/tracker/test/issueentry_test.py
@@ -0,0 +1,1045 @@
+# Copyright 2016 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
+
+"""Unittests for the issueentry servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mox
+import time
+import unittest
+
+import ezt
+
+from google.appengine.ext import testbed
+from mock import Mock, patch
+import webapp2
+
+from framework import framework_bizobj
+from framework import framework_views
+from framework import permissions
+from services import service_manager
+from services import template_svc
+from testing import fake
+from testing import testing_helpers
+from tracker import issueentry
+from tracker import tracker_bizobj
+from proto import tracker_pb2
+from proto import user_pb2
+
+
+class IssueEntryTest(unittest.TestCase):
+ def setUp(self):
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_datastore_v3_stub()
+ # Load queue.yaml.
+
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ template=Mock(spec=template_svc.TemplateService),
+ features=fake.FeaturesService())
+ self.project = self.services.project.TestAddProject('proj', project_id=987)
+ request = webapp2.Request.blank('/p/proj/issues/entry')
+ response = webapp2.Response()
+ self.servlet = issueentry.IssueEntry(
+ request, response, services=self.services)
+ self.user = self.services.user.TestAddUser('to_pass_tests', 0)
+ self.services.features.TestAddHotlist(
+ name='dontcare', summary='', owner_ids=[0])
+ self.template = testing_helpers.DefaultTemplates()[1]
+ self.services.template.GetTemplateByName = Mock(return_value=self.template)
+ self.services.template.GetTemplateSetForProject = Mock(
+ return_value=[(1, 'name', False)])
+
+ # Set-up for testing hotlist parsing.
+ # Scenario:
+ # Users: U1, U2, and U3
+ # Hotlists:
+ # H1: owned by U1 (private)
+ # H2: owned by U2, can be edited by U1 (private)
+ # H2: owned by U3, can be edited by U1 and U2 (public)
+ self.cnxn = fake.MonorailConnection()
+ self.U1 = self.services.user.TestAddUser('U1', 111)
+ self.U2 = self.services.user.TestAddUser('U2', 222)
+ self.U3 = self.services.user.TestAddUser('U3', 333)
+
+ self.H1 = self.services.features.TestAddHotlist(
+ name='H1', summary='', owner_ids=[111], is_private=True)
+ self.H2 = self.services.features.TestAddHotlist(
+ name='H2', summary='', owner_ids=[222], editor_ids=[111],
+ is_private=True)
+ self.H2_U3 = self.services.features.TestAddHotlist(
+ name='H2', summary='', owner_ids=[333], editor_ids=[111, 222],
+ is_private=False)
+
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.testbed.deactivate()
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testAssertBasePermission(self):
+ """Permit users with CREATE_ISSUE."""
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', services=self.services,
+ perms=permissions.EMPTY_PERMISSIONSET)
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', services=self.services,
+ perms=permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET)
+ self.servlet.AssertBasePermission(mr)
+
+ def testDiscardUnusedTemplateLabelPrefixes(self):
+ labels = ['pre-val', 'other-value', 'oneword', 'x', '-y', '-w-z', '', '-']
+ self.assertEqual(labels,
+ issueentry._DiscardUnusedTemplateLabelPrefixes(labels))
+
+ labels = ['prefix-value', 'other-?', 'third-', '', '-', '-?']
+ self.assertEqual(['prefix-value', 'third-', '', '-'],
+ issueentry._DiscardUnusedTemplateLabelPrefixes(labels))
+
+ def testGatherPageData(self):
+ user = self.services.user.TestAddUser('user@invalid', 100)
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', services=self.services)
+ mr.perms = permissions.PermissionSet(
+ [permissions.CREATE_ISSUE, permissions.EDIT_ISSUE])
+ mr.auth.user_view = framework_views.MakeUserView(
+ 'cnxn', self.services.user, 100)
+ mr.auth.effective_ids = {100}
+ mr.template_name = 'rutabaga'
+
+ self.mox.StubOutWithMock(self.services.user, 'GetUser')
+ self.services.user.GetUser(
+ mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(user)
+ self.mox.ReplayAll()
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ config.field_defs = [
+ tracker_bizobj.MakeFieldDef(
+ 22, mr.project_id, 'NotEnum', tracker_pb2.FieldTypes.STR_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False),
+ tracker_bizobj.MakeFieldDef(
+ 23, mr.project_id, 'Choices', tracker_pb2.FieldTypes.ENUM_TYPE,
+ None, '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False),
+ tracker_bizobj.MakeFieldDef(
+ 24,
+ mr.project_id,
+ 'RestrictedField',
+ tracker_pb2.FieldTypes.STR_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'doc',
+ False,
+ is_restricted_field=True)
+ ]
+ self.services.config.StoreConfig(mr.cnxn, config)
+ template = tracker_pb2.TemplateDef(
+ labels=['NotEnum-Not-Masked', 'Choices-Masked'])
+ self.services.template.GetTemplateByName.return_value = template
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
+ self.assertEqual(page_data['initial_owner'], 'user@invalid')
+ self.assertEqual(page_data['initial_status'], 'New')
+ self.assertTrue(page_data['clear_summary_on_click'])
+ self.assertTrue(page_data['must_edit_summary'])
+ self.assertEqual(page_data['labels'], ['NotEnum-Not-Masked'])
+ self.assertEqual(page_data['offer_templates'], ezt.boolean(False))
+ self.assertEqual(page_data['fields'][0].is_editable, ezt.boolean(True))
+ self.assertEqual(page_data['fields'][1].is_editable, ezt.boolean(True))
+ self.assertEqual(page_data['fields'][2].is_editable, ezt.boolean(False))
+ self.assertEqual(page_data['uneditable_fields'], ezt.boolean(True))
+
+ def testGatherPageData_Approvals(self):
+ user = self.services.user.TestAddUser('user@invalid', 100)
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', services=self.services)
+ mr.auth.user_view = framework_views.MakeUserView(
+ 'cnxn', self.services.user, 100)
+ mr.template_name = 'rutabaga'
+
+ self.mox.StubOutWithMock(self.services.user, 'GetUser')
+ self.services.user.GetUser(
+ mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(user)
+ self.mox.ReplayAll()
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ config.field_defs = [
+ tracker_bizobj.MakeFieldDef(
+ 24, mr.project_id, 'UXReview',
+ tracker_pb2.FieldTypes.APPROVAL_TYPE, None, '', False, False,
+ False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)]
+ self.services.config.StoreConfig(mr.cnxn, config)
+ template = tracker_pb2.TemplateDef()
+ template.phases = [tracker_pb2.Phase(
+ phase_id=1, rank=4, name='Stable')]
+ template.approval_values = [tracker_pb2.ApprovalValue(
+ approval_id=24, phase_id=1,
+ status=tracker_pb2.ApprovalStatus.NEEDS_REVIEW)]
+ self.services.template.GetTemplateByName.return_value = template
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
+ self.assertEqual(page_data['approvals'][0].field_name, 'UXReview')
+ self.assertEqual(page_data['initial_phases'][0],
+ tracker_pb2.Phase(phase_id=1, name='Stable', rank=4))
+ self.assertEqual(page_data['prechecked_approvals'], ['24_phase_0'])
+ self.assertEqual(page_data['required_approval_ids'], [24])
+
+ # phase fields row shown when config contains phase fields.
+ config.field_defs.append(tracker_bizobj.MakeFieldDef(
+ 26, mr.project_id, 'GateTarget',
+ tracker_pb2.FieldTypes.INT_TYPE, None, '', False, False, False,
+ None, None, '', False, '', '', tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action', 'doc', False, is_phase_field=True))
+ self.services.config.StoreConfig(mr.cnxn, config)
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(page_data['issue_phase_names'], ['stable'])
+
+ # approval subfields in config hidden when chosen template does not contain
+ # its parent approval
+ template = tracker_pb2.TemplateDef()
+ self.services.template.GetTemplateByName.return_value = template
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(page_data['approvals'], [])
+ # phase fields row hidden when template has no phases
+ self.assertEqual(page_data['issue_phase_names'], [])
+
+ # TODO(jojwang): monorail:6305, remove this test when Edit perms
+ # for field values are implemented.
+ def testGatherPageData_FLTSpecialFields(self):
+ user = self.services.user.TestAddUser('user@invalid', 100)
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', services=self.services)
+ mr.auth.user_view = framework_views.MakeUserView(
+ 'cnxn', self.services.user, 100)
+ mr.template_name = 'rutabaga'
+
+ self.mox.StubOutWithMock(self.services.user, 'GetUser')
+ self.services.user.GetUser(
+ mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(user)
+ self.mox.ReplayAll()
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ config.field_defs = [
+ tracker_bizobj.MakeFieldDef(
+ 25, mr.project_id, 'nOtice',
+ tracker_pb2.FieldTypes.STR_TYPE, None, '', False, False,
+ False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False),
+ tracker_bizobj.MakeFieldDef(
+ 24, mr.project_id, 'M-Target',
+ tracker_pb2.FieldTypes.STR_TYPE, None, '', False, False,
+ False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False),
+ tracker_bizobj.MakeFieldDef(
+ 25, mr.project_id, 'whitepaper',
+ tracker_pb2.FieldTypes.STR_TYPE, None, '', False, False,
+ False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False),
+ tracker_bizobj.MakeFieldDef(
+ 25, mr.project_id, 'm-approved',
+ tracker_pb2.FieldTypes.STR_TYPE, None, '', False, False,
+ False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False),
+ ]
+
+ self.services.config.StoreConfig(mr.cnxn, config)
+ template = tracker_pb2.TemplateDef()
+ self.services.template.GetTemplateByName.return_value = template
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
+ self.assertEqual(page_data['fields'][0].field_name, 'M-Target')
+ self.assertEqual(len(page_data['fields']), 1)
+
+ def testGatherPageData_DefaultOwnerAvailability(self):
+ user = self.services.user.TestAddUser('user@invalid', 100)
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', services=self.services)
+ mr.auth.user_view = framework_views.MakeUserView(
+ 'cnxn', self.services.user, 100)
+ mr.template_name = 'rutabaga'
+
+ self.mox.StubOutWithMock(self.services.user, 'GetUser')
+ self.services.user.GetUser(
+ mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(user)
+ self.mox.ReplayAll()
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
+ self.assertEqual(page_data['initial_owner'], 'user@invalid')
+ self.assertEqual(page_data['owner_avail_state'], 'never')
+ self.assertEqual(
+ page_data['owner_avail_message_short'],
+ 'User never visited')
+
+ user.last_visit_timestamp = int(time.time())
+ mr.auth.user_view = framework_views.MakeUserView(
+ 'cnxn', self.services.user, 100)
+ page_data = self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
+ self.assertEqual(page_data['initial_owner'], 'user@invalid')
+ self.assertEqual(page_data['owner_avail_state'], None)
+ self.assertEqual(page_data['owner_avail_message_short'], '')
+
+ def testGatherPageData_TemplateAllowsKeepingSummary(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', services=self.services)
+ mr.auth.user_view = framework_views.StuffUserView(100, 'user@invalid', True)
+ mr.template_name = 'rutabaga'
+ user = self.services.user.TestAddUser('user@invalid', 100)
+
+ self.mox.StubOutWithMock(self.services.user, 'GetUser')
+ self.services.user.GetUser(
+ mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(user)
+ self.mox.ReplayAll()
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ self.services.config.StoreConfig(mr.cnxn, config)
+ self.template.summary_must_be_edited = False
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
+ self.assertEqual(page_data['initial_owner'], 'user@invalid')
+ self.assertEqual(page_data['initial_status'], 'New')
+ self.assertFalse(page_data['clear_summary_on_click'])
+ self.assertFalse(page_data['must_edit_summary'])
+
+ def testGatherPageData_DeepLinkSetsSummary(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry?summary=foo', services=self.services)
+ mr.auth.user_view = framework_views.StuffUserView(100, 'user@invalid', True)
+ user = self.services.user.TestAddUser('user@invalid', 100)
+ mr.template_name = 'rutabaga'
+
+ self.mox.StubOutWithMock(self.services.user, 'GetUser')
+ self.services.user.GetUser(
+ mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(user)
+ self.mox.ReplayAll()
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
+ self.assertEqual(page_data['initial_owner'], 'user@invalid')
+ self.assertEqual(page_data['initial_status'], 'New')
+ self.assertFalse(page_data['clear_summary_on_click'])
+ self.assertTrue(page_data['must_edit_summary'])
+
+ @patch('framework.framework_bizobj.UserIsInProject')
+ def testGatherPageData_MembersOnlyTemplatesExcluded(self,
+ mockUserIsInProject):
+ """Templates with members_only=True are excluded from results
+ when the user is not a member of the project."""
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', services=self.services)
+ mr.auth.user_view = framework_views.StuffUserView(100, 'user@invalid', True)
+ user = self.services.user.TestAddUser('user@invalid', 100)
+ mr.template_name = 'rutabaga'
+ self.services.template.GetTemplateSetForProject = Mock(
+ return_value=[(1, 'one', False), (2, 'two', True)])
+ mockUserIsInProject.return_value = False
+
+ self.mox.StubOutWithMock(self.services.user, 'GetUser')
+ self.services.user.GetUser(
+ mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(user)
+ self.mox.ReplayAll()
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
+ self.assertEqual(page_data['config'].template_names, ['one'])
+
+ @patch('framework.framework_bizobj.UserIsInProject')
+ def testGatherPageData_DefaultTemplatesMember(self, mockUserIsInProject):
+ """If no template is specified, the default one is used based on
+ whether the user is a project member."""
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', services=self.services)
+ mr.auth.user_view = framework_views.StuffUserView(100, 'user@invalid', True)
+ user = self.services.user.TestAddUser('user@invalid', 100)
+ self.services.template.GetTemplateSetForProject = Mock(
+ return_value=[(1, 'one', False), (2, 'two', True)])
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ config.default_template_for_users = 456
+ config.default_template_for_developers = 789
+ self.services.config.StoreConfig(mr.cnxn, config)
+
+ mockUserIsInProject.return_value = True
+ self.services.template.GetTemplateById = Mock(return_value=self.template)
+ self.mox.StubOutWithMock(self.services.user, 'GetUser')
+ self.services.user.GetUser(
+ mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(user)
+
+ self.mox.ReplayAll()
+ self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
+
+ call_args = self.services.template.GetTemplateById.call_args[0]
+ self.assertEqual(call_args[1], 789)
+
+ @patch('framework.framework_bizobj.UserIsInProject')
+ def testGatherPageData_DefaultTemplatesNonMember(self, mockUserIsInProject):
+ """If no template is specified, the default one is used based on
+ whether the user is not a project member."""
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', services=self.services)
+ mr.auth.user_view = framework_views.StuffUserView(100, 'user@invalid', True)
+ user = self.services.user.TestAddUser('user@invalid', 100)
+ self.services.template.GetTemplateSetForProject = Mock(
+ return_value=[(1, 'one', False), (2, 'two', True)])
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ config.default_template_for_users = 456
+ config.default_template_for_developers = 789
+ self.services.config.StoreConfig(mr.cnxn, config)
+
+ mockUserIsInProject.return_value = False
+ self.services.template.GetTemplateById = Mock(return_value=self.template)
+ self.mox.StubOutWithMock(self.services.user, 'GetUser')
+ self.services.user.GetUser(
+ mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(user)
+
+ self.mox.ReplayAll()
+ self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
+
+ call_args = self.services.template.GetTemplateById.call_args[0]
+ self.assertEqual(call_args[1], 456)
+
+ def testGatherPageData_MissingDefaultTemplates(self):
+ """If the default templates were deleted, pick the first template."""
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', services=self.services)
+ mr.auth.user_view = framework_views.StuffUserView(100, 'user@invalid', True)
+ user = self.services.user.TestAddUser('user@invalid', 100)
+ self.services.template.GetTemplateSetForProject = Mock(
+ return_value=[(1, 'one', False), (2, 'two', True)])
+
+ self.services.template.GetTemplateById.return_value = None
+ self.services.template.GetProjectTemplates.return_value = [
+ tracker_pb2.TemplateDef(members_only=True),
+ tracker_pb2.TemplateDef(members_only=False)]
+ self.mox.StubOutWithMock(self.services.user, 'GetUser')
+ self.services.user.GetUser(
+ mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(user)
+
+ self.mox.ReplayAll()
+ page_data = self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
+
+ self.assertTrue(self.services.template.GetProjectTemplates.called)
+ self.assertTrue(page_data['config'].template_view.members_only)
+
+ def testGatherPageData_IncorrectTemplate(self):
+ """The handler shouldn't error out if passed a non-existent template."""
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', services=self.services)
+ mr.auth.user_view = framework_views.StuffUserView(100, 'user@invalid', True)
+ mr.template_name = 'rutabaga'
+
+ user = self.services.user.TestAddUser('user@invalid', 100)
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ config.default_template_for_users = 456
+ config.default_template_for_developers = 789
+ self.services.config.StoreConfig(mr.cnxn, config)
+
+ self.services.template.GetTemplateSetForProject.return_value = [
+ (1, 'one', False), (2, 'two', True)]
+ self.services.template.GetTemplateByName.return_value = None
+ self.services.template.GetTemplateById.return_value = \
+ tracker_pb2.TemplateDef(template_id=123, labels=['yo'])
+ self.services.template.GetProjectTemplates.return_value = [
+ tracker_pb2.TemplateDef(labels=['no']),
+ tracker_pb2.TemplateDef(labels=['maybe'])]
+ self.mox.StubOutWithMock(self.services.user, 'GetUser')
+ self.services.user.GetUser(
+ mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(user)
+
+ self.mox.ReplayAll()
+ page_data = self.servlet.GatherPageData(mr)
+ self.mox.VerifyAll()
+
+ self.assertTrue(self.services.template.GetTemplateByName.called)
+ self.assertTrue(self.services.template.GetTemplateById.called)
+ self.assertFalse(self.services.template.GetProjectTemplates.called)
+ self.assertEqual(page_data['config'].template_view.label0, 'yo')
+
+ def testGatherPageData_RestrictNewIssues(self):
+ """Users with this pref set default to reporting issues with R-V-G."""
+ self.mox.ReplayAll()
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', services=self.services)
+ mr.auth.user_view = framework_views.StuffUserView(100, 'user@invalid', True)
+ user = self.services.user.TestAddUser('user@invalid', 100)
+ self.services.user.GetUser = Mock(return_value=user)
+ self.services.template.GetTemplateById = Mock(return_value=self.template)
+
+ mr.auth.user_id = 100
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertNotIn('Restrict-View-Google', page_data['labels'])
+
+ pref = user_pb2.UserPrefValue(name='restrict_new_issues', value='true')
+ self.services.user.SetUserPrefs(self.cnxn, 100, [pref])
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertIn('Restrict-View-Google', page_data['labels'])
+
+ def testGatherHelpData_Anon(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', project=self.project)
+ mr.auth.user_pb = user_pb2.User()
+ mr.auth.user_id = 0
+
+ help_data = self.servlet.GatherHelpData(mr, {})
+ self.assertEqual(
+ {'account_cue': None,
+ 'cue': None,
+ 'is_privileged_domain_user': None},
+ help_data)
+
+ def testGatherHelpData_NewUser(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', project=self.project)
+ mr.auth.user_pb = user_pb2.User(user_id=111)
+ mr.auth.user_id = 111
+
+ help_data = self.servlet.GatherHelpData(mr, {})
+ self.assertEqual(
+ {'account_cue': None,
+ 'cue': 'privacy_click_through',
+ 'is_privileged_domain_user': None},
+ help_data)
+
+ def testGatherHelpData_AlreadyClickedThroughPrivacy(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', project=self.project)
+ mr.auth.user_pb = user_pb2.User(user_id=111)
+ mr.auth.user_id = 111
+ self.services.user.SetUserPrefs(
+ self.cnxn, 111,
+ [user_pb2.UserPrefValue(name='privacy_click_through', value='true')])
+
+ help_data = self.servlet.GatherHelpData(mr, {})
+ self.assertEqual(
+ {'account_cue': None,
+ 'cue': 'code_of_conduct',
+ 'is_privileged_domain_user': None},
+ help_data)
+
+ def testGatherHelpData_DismissedEverything(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', project=self.project)
+ mr.auth.user_pb = user_pb2.User(user_id=111)
+ mr.auth.user_id = 111
+ self.services.user.SetUserPrefs(
+ self.cnxn, 111,
+ [user_pb2.UserPrefValue(name='privacy_click_through', value='true'),
+ user_pb2.UserPrefValue(name='code_of_conduct', value='true')])
+
+ help_data = self.servlet.GatherHelpData(mr, {})
+ self.assertEqual(
+ {'account_cue': None,
+ 'cue': None,
+ 'is_privileged_domain_user': None},
+ help_data)
+
+ @patch('framework.cloud_tasks_helpers.create_task')
+ def testProcessFormData_RedirectToEnteredIssue(self, _create_task_mock):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', project=self.project)
+ mr.auth.user_view = framework_views.StuffUserView(100, 'user@invalid', True)
+ mr.template_name = 'rutabaga'
+ mr.auth.effective_ids = set([100])
+ post_data = fake.PostData(
+ template_name=['rutabaga'],
+ summary=['fake summary'],
+ comment=['fake comment'],
+ status=['New'])
+
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessFormData(mr, post_data)
+
+ self.mox.VerifyAll()
+ self.assertTrue('/p/proj/issues/detail?id=' in url)
+
+ @patch('framework.cloud_tasks_helpers.create_task')
+ def testProcessFormData_AcceptWithFields(self, _create_task_mock):
+ """We can create new issues with custom fields (restricted or not)."""
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', project=self.project)
+ mr.auth.user_view = framework_views.StuffUserView(100, 'admin@test', True)
+ mr.template_name = 'rutabaga'
+ mr.auth.effective_ids = set([100])
+ config = self.services.config.GetProjectConfig(
+ mr.cnxn, self.project.project_id)
+ config.field_defs = [
+ tracker_bizobj.MakeFieldDef(
+ 1, 789, 'NonRestrictedField', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'NonRestrictedField',
+ False),
+ tracker_bizobj.MakeFieldDef(
+ 2,
+ 789,
+ 'RestrictedField',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'RestrictedField',
+ False,
+ is_restricted_field=True),
+ tracker_bizobj.MakeFieldDef(
+ 3,
+ 789,
+ 'RestrictedEnumField',
+ tracker_pb2.FieldTypes.ENUM_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'RestrictedEnumField',
+ False,
+ is_restricted_field=True)
+ ]
+ self.services.config.StoreConfig(mr.cnxn, config)
+ post_data = fake.PostData(
+ template_name=['rutabaga'],
+ summary=['fake summary'],
+ comment=['fake comment'],
+ custom_1=['3'],
+ custom_2=['7'],
+ label=['RestrictedEnumField-7'],
+ status=['New'])
+
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessFormData(mr, post_data)
+
+ self.mox.VerifyAll()
+ self.assertTrue('/p/proj/issues/detail?id=' in url)
+ field_values = self.services.issue.issues_by_project[987][1].field_values
+ self.assertEqual(
+ self.services.issue.issues_by_project[987][1].labels,
+ ['RestrictedEnumField-7'])
+ self.assertEqual(field_values[0].int_value, 3)
+ self.assertEqual(field_values[1].int_value, 7)
+
+ @patch('framework.cloud_tasks_helpers.create_task')
+ def testProcessFormData_AcceptEnforceTemplateRestrictedDefaultValues(
+ self, _create_task_mock):
+ """The template applies default vals on fields that the user cannot edit."""
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', project=self.project)
+ mr.auth.user_view = framework_views.StuffUserView(100, 'admin@test', True)
+ mr.template_name = 'rutabaga'
+ mr.auth.effective_ids = set([100])
+ mr.perms = permissions.PermissionSet([])
+ config = self.services.config.GetProjectConfig(
+ mr.cnxn, self.project.project_id)
+ config.field_defs = [
+ tracker_bizobj.MakeFieldDef(
+ 1, 789, 'NonRestrictedField', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'NonRestrictedField',
+ False),
+ tracker_bizobj.MakeFieldDef(
+ 2,
+ 789,
+ 'RestrictedField',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'RestrictedField',
+ False,
+ is_restricted_field=True),
+ tracker_bizobj.MakeFieldDef(
+ 3,
+ 789,
+ 'RestrictedEnumField',
+ tracker_pb2.FieldTypes.ENUM_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'RestrictedEnumField',
+ False,
+ is_restricted_field=True)
+ ]
+ self.services.config.StoreConfig(mr.cnxn, config)
+ post_data = fake.PostData(
+ template_name=['rutabaga'],
+ summary=['fake summary'],
+ comment=['fake comment'],
+ custom_1=['3'],
+ label=['Hey'],
+ status=['New'])
+
+ temp_restricted_fv = tracker_bizobj.MakeFieldValue(
+ 2, 3737, None, None, None, None, False)
+ self.template.field_values.append(temp_restricted_fv)
+ self.template.labels.append('RestrictedEnumField-b')
+
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessFormData(mr, post_data)
+
+ self.mox.VerifyAll()
+ self.assertTrue('/p/proj/issues/detail?id=' in url)
+ field_values = self.services.issue.issues_by_project[987][1].field_values
+ self.assertEqual(
+ self.services.issue.issues_by_project[987][1].labels,
+ ['Hey', 'RestrictedEnumField-b'])
+ self.assertEqual(field_values[0].int_value, 3)
+ self.assertEqual(field_values[1].int_value, 3737)
+
+ def testProcessFormData_RejectRestrictedFields(self):
+ """We raise an AssertionError when restricted fields are set w/o perms."""
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', project=self.project)
+ mr.auth.user_view = framework_views.StuffUserView(
+ 100, 'non-admin@test', True)
+ mr.template_name = 'rutabaga'
+ mr.auth.effective_ids = set([100])
+ mr.perms = permissions.PermissionSet([])
+ config = self.services.config.GetProjectConfig(
+ mr.cnxn, self.project.project_id)
+ config.field_defs = [
+ tracker_bizobj.MakeFieldDef(
+ 1, 789, 'NonRestrictedField', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'NonRestrictedField',
+ False),
+ tracker_bizobj.MakeFieldDef(
+ 2,
+ 789,
+ 'RestrictedField',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'RestrictedField',
+ False,
+ is_restricted_field=True),
+ tracker_bizobj.MakeFieldDef(
+ 3,
+ 789,
+ 'RestrictedEnumField',
+ tracker_pb2.FieldTypes.ENUM_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'RestrictedEnumField',
+ False,
+ is_restricted_field=True)
+ ]
+ self.services.config.StoreConfig(mr.cnxn, config)
+ post_data_add_fv = fake.PostData(
+ template_name=['rutabaga'],
+ summary=['fake summary'],
+ comment=['fake comment'],
+ custom_1=['3'],
+ custom_2=['7'],
+ status=['New'])
+ post_data_label_edits_enum = fake.PostData(
+ template_name=['rutabaga'],
+ summary=['fake summary'],
+ comment=['fake comment'],
+ label=['RestrictedEnumField-7'],
+ status=['New'])
+
+ self.assertRaises(
+ AssertionError, self.servlet.ProcessFormData, mr, post_data_add_fv)
+ self.assertRaises(
+ AssertionError, self.servlet.ProcessFormData, mr,
+ post_data_label_edits_enum)
+
+ def testProcessFormData_RejectPlacedholderSummary(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry')
+ mr.auth.user_view = framework_views.StuffUserView(100, 'user@invalid', True)
+ mr.perms = permissions.USER_PERMISSIONSET
+ mr.template_name = 'rutabaga'
+ post_data = fake.PostData(
+ template_name=['rutabaga'],
+ summary=[issueentry.PLACEHOLDER_SUMMARY],
+ comment=['fake comment'],
+ status=['New'])
+
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ mr, component_required=None, fields=[], initial_blocked_on='',
+ initial_blocking='', initial_cc='', initial_comment='fake comment',
+ initial_components='', initial_owner='', initial_status='New',
+ initial_summary='Enter one-line summary', initial_hotlists='',
+ labels=[], template_name='rutabaga')
+ self.mox.ReplayAll()
+
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual('Summary is required', mr.errors.summary)
+ self.assertIsNone(url)
+
+ def testProcessFormData_RejectUnmodifiedTemplate(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry')
+ mr.perms = permissions.USER_PERMISSIONSET
+ mr.auth.user_view = framework_views.StuffUserView(100, 'user@invalid', True)
+ post_data = fake.PostData(
+ template_name=['rutabaga'],
+ summary=['Nya nya I modified the summary'],
+ comment=[self.template.content],
+ status=['New'])
+
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ mr, component_required=None, fields=[], initial_blocked_on='',
+ initial_blocking='', initial_cc='',
+ initial_comment=self.template.content, initial_components='',
+ initial_owner='', initial_status='New',
+ initial_summary='Nya nya I modified the summary', initial_hotlists='',
+ labels=[], template_name='rutabaga')
+ self.mox.ReplayAll()
+
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual('Template must be filled out.', mr.errors.comment)
+ self.assertIsNone(url)
+
+ def testProcessFormData_RejectNonexistentHotlist(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', user_info={'user_id': 111})
+ entered_hotlists = 'H3'
+ post_data = fake.PostData(hotlists=[entered_hotlists],
+ template_name=['rutabaga'])
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ mr, component_required=None, fields=[], initial_blocked_on='',
+ initial_blocking='', initial_cc='', initial_comment='',
+ initial_components='', initial_owner='', initial_status='',
+ initial_summary='', initial_hotlists=entered_hotlists, labels=[],
+ template_name='rutabaga')
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual('You have no hotlist(s) named: H3', mr.errors.hotlists)
+ self.assertIsNone(url)
+
+ def testProcessFormData_RejectNonexistentHotlistOwner(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', user_info={'user_id': 111})
+ entered_hotlists = 'abc:H1'
+ post_data = fake.PostData(hotlists=[entered_hotlists],
+ template_name=['rutabaga'])
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ mr, component_required=None, fields=[], initial_blocked_on='',
+ initial_blocking='', initial_cc='', initial_comment='',
+ initial_components='', initial_owner='', initial_status='',
+ initial_summary='', initial_hotlists=entered_hotlists, labels=[],
+ template_name='rutabaga')
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual('You have no hotlist(s) owned by: abc', mr.errors.hotlists)
+ self.assertIsNone(url)
+
+ def testProcessFormData_RejectInvalidHotlistName(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', user_info={'user_id': 111})
+ entered_hotlists = 'U1:H2'
+ post_data = fake.PostData(hotlists=[entered_hotlists],
+ template_name=['rutabaga'])
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ mr, component_required=None, fields=[], initial_blocked_on='',
+ initial_blocking='', initial_cc='', initial_comment='',
+ initial_components='', initial_owner='', initial_status='',
+ initial_summary='', initial_hotlists=entered_hotlists, labels=[],
+ template_name='rutabaga')
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual('Not in your hotlist(s): U1:H2', mr.errors.hotlists)
+ self.assertIsNone(url)
+
+ def testProcessFormData_RejectDeprecatedComponent(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry',
+ user_info={'user_id': 111},
+ project=self.project)
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ config.component_defs = [
+ tracker_bizobj.MakeComponentDef(
+ 1, mr.project_id, 'active', '', False, [], [], 0, 0),
+ tracker_bizobj.MakeComponentDef(
+ 2, mr.project_id, 'notactive', '', True, [], [], 0, 0),
+ ]
+ self.services.config.StoreConfig(mr.cnxn, config)
+ print(config)
+ post_data = fake.PostData(
+ template_name=['rutabaga'],
+ summary=['fake summary'],
+ comment=['fake comment'],
+ components=['notactive'])
+
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ mr,
+ component_required=None,
+ fields=[],
+ initial_blocked_on='',
+ initial_blocking='',
+ initial_cc='',
+ initial_comment='fake comment',
+ initial_components='notactive',
+ initial_owner='',
+ initial_status='',
+ initial_summary='fake summary',
+ initial_hotlists='',
+ labels=[],
+ template_name='rutabaga')
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual(mr.errors.components, 'Undefined or deprecated component')
+ self.assertIsNone(url)
+
+ @patch('framework.cloud_tasks_helpers.create_task')
+ def testProcessFormData_TemplateNameMissing(self, _create_task_mock):
+ """POST doesn't fail if no template_name is passed."""
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', project=self.project)
+ mr.auth.user_view = framework_views.StuffUserView(100, 'user@invalid', True)
+ mr.auth.effective_ids = set([100])
+
+ self.services.template.GetTemplateById.return_value = None
+ self.services.template.GetProjectTemplates.return_value = [
+ tracker_pb2.TemplateDef(members_only=True, content=''),
+ tracker_pb2.TemplateDef(members_only=False, content='')]
+ post_data = fake.PostData(
+ summary=['fake summary'],
+ comment=['fake comment'],
+ status=['New'])
+
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessFormData(mr, post_data)
+
+ self.mox.VerifyAll()
+ self.assertTrue('/p/proj/issues/detail?id=' in url)
+
+ @patch('framework.cloud_tasks_helpers.create_task')
+ def testProcessFormData_AcceptsFederatedReferences(self, _create_task_mock):
+ """ProcessFormData accepts federated references."""
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/entry', project=self.project)
+ mr.auth.user_view = framework_views.StuffUserView(100, 'user@invalid', True)
+ mr.auth.effective_ids = set([100])
+
+ post_data = fake.PostData(
+ summary=['fake summary'],
+ comment=['fake comment'],
+ status=['New'],
+ template_name='rutabaga',
+ blocking=['b/123, b/987'],
+ blockedon=['b/456, b/654'])
+
+ self.mox.ReplayAll()
+ self.servlet.ProcessFormData(mr, post_data)
+
+ self.mox.VerifyAll()
+ self.assertIsNone(mr.errors.blockedon)
+ self.assertIsNone(mr.errors.blocking)
+
+ def testAttachDefaultApprovers(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ config.approval_defs = [
+ tracker_pb2.ApprovalDef(
+ approval_id=23, approver_ids=[222], survey='Question?'),
+ tracker_pb2.ApprovalDef(
+ approval_id=24, approver_ids=[111], survey='Question?')]
+ approval_values = [tracker_pb2.ApprovalValue(
+ approval_id=24, phase_id=1,
+ status=tracker_pb2.ApprovalStatus.NEEDS_REVIEW)]
+ issueentry._AttachDefaultApprovers(config, approval_values)
+ self.assertEqual(approval_values[0].approver_ids, [111])
+
+ # TODO(aneeshm): add a test for the ambiguous hotlist name case; it works
+ # correctly when tested locally, but for some reason doesn't in the test
+ # environment. Probably a result of some quirk in fake.py?
diff --git a/tracker/test/issueexport_test.py b/tracker/test/issueexport_test.py
new file mode 100644
index 0000000..4e70ab7
--- /dev/null
+++ b/tracker/test/issueexport_test.py
@@ -0,0 +1,163 @@
+# Copyright 2016 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
+
+"""Unittests for the issueexport servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from mock import Mock, patch
+
+from framework import permissions
+from proto import tracker_pb2
+from services import service_manager
+from testing import testing_helpers
+from testing import fake
+from tracker import issueexport
+
+
+class IssueExportTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ project=fake.ProjectService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ issue_star=fake.IssueStarService(),
+ )
+ self.cnxn = 'fake connection'
+ self.project = self.services.project.TestAddProject('proj', project_id=789)
+ self.servlet = issueexport.IssueExport(
+ 'req', 'res', services=self.services)
+ self.jsonfeed = issueexport.IssueExportJSON(
+ 'req', 'res', services=self.services)
+ self.mr = testing_helpers.MakeMonorailRequest(
+ project=self.project, perms=permissions.OWNER_ACTIVE_PERMISSIONSET)
+ self.mr.can = 1
+
+ def testAssertBasePermission(self):
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.AssertBasePermission, self.mr)
+ self.mr.auth.user_pb.is_site_admin = True
+ self.servlet.AssertBasePermission(self.mr)
+
+ @patch('time.time')
+ def testHandleRequest(self, mockTime):
+ mockTime.return_value = 1234
+ self.services.issue.GetAllIssuesInProject = Mock(return_value=[])
+ self.services.issue.GetCommentsForIssues = Mock(return_value={})
+ self.services.issue_star.LookupItemsStarrers = Mock(return_value={})
+ self.services.user.LookupUserEmails = Mock(
+ return_value={111: 'user1@test.com', 222: 'user2@test.com'})
+
+ self.mr.project_name = self.project.project_name
+ json_data = self.jsonfeed.HandleRequest(self.mr)
+
+ self.assertEqual(json_data['metadata'],
+ {'version': 1, 'who': None, 'when': 1234,
+ 'project': 'proj', 'start': 0, 'num': 100})
+ self.assertEqual(json_data['issues'], [])
+ self.assertItemsEqual(
+ json_data['emails'], ['user1@test.com', 'user2@test.com'])
+
+ # TODO(jojwang): test attachments, amendments, comment details
+ def testMakeIssueJSON(self):
+ config = self.services.config.GetProjectConfig(
+ self.cnxn, 789)
+ config.field_defs.extend(
+ [tracker_pb2.FieldDef(
+ field_id=1, field_name='UXReview',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE),
+ tracker_pb2.FieldDef(
+ field_id=2, field_name='approvalsubfield',
+ field_type=tracker_pb2.FieldTypes.STR_TYPE, approval_id=1),
+ tracker_pb2.FieldDef(
+ field_id=3, field_name='phasefield',
+ field_type=tracker_pb2.FieldTypes.INT_TYPE, is_phase_field=True),
+ tracker_pb2.FieldDef(
+ field_id=4, field_name='normalfield',
+ field_type=tracker_pb2.FieldTypes.STR_TYPE)
+ ])
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ phases = [tracker_pb2.Phase(phase_id=1, name='Phase1', rank=1),
+ tracker_pb2.Phase(phase_id=2, name='Phase2', rank=2)]
+ avs = [tracker_pb2.ApprovalValue(
+ approval_id=1, status=tracker_pb2.ApprovalStatus.APPROVED,
+ setter_id=111, set_on=7, approver_ids=[333, 444], phase_id=1)]
+ fvs = [tracker_pb2.FieldValue(field_id=2, str_value='two'),
+ tracker_pb2.FieldValue(field_id=3, int_value=3, phase_id=2),
+ tracker_pb2.FieldValue(field_id=4, str_value='four')]
+ labels = ['test', 'Type-FLT-Launch']
+
+ issue = fake.MakeTestIssue(
+ self.project.project_id, 1, 'summary', 'Open', 111, labels=labels,
+ issue_id=78901, reporter_id=222, opened_timestamp=1,
+ closed_timestamp=2, modified_timestamp=3, project_name='project',
+ field_values=fvs, phases=phases, approval_values=avs)
+
+ email_dict = {111: 'user1@test.com', 222: 'user2@test.com',
+ 333: 'user3@test.com', 444: 'user4@test.com'}
+ comment_list = [
+ tracker_pb2.IssueComment(content='simple'),
+ tracker_pb2.IssueComment(
+ content='issue desc', is_description=True)]
+ starrer_id_list = [222, 333]
+
+ issue_JSON = self.jsonfeed._MakeIssueJSON(
+ self.mr, issue, email_dict, comment_list, starrer_id_list)
+ expected_JSON = {
+ 'local_id': 1,
+ 'reporter': 'user2@test.com',
+ 'summary': 'summary',
+ 'owner': 'user1@test.com',
+ 'status': 'Open',
+ 'cc': [],
+ 'labels': labels,
+ 'phases': [{'id': 1, 'name': 'Phase1', 'rank': 1},
+ {'id': 2, 'name': 'Phase2', 'rank': 2}],
+ 'fields': [
+ {'field': 'approvalsubfield',
+ 'phase': None,
+ 'approval': 'UXReview',
+ 'str_value': 'two'},
+ {'field': 'phasefield',
+ 'phase': 'Phase2',
+ 'int_value': 3},
+ {'field': 'normalfield',
+ 'phase': None,
+ 'str_value': 'four'}],
+ 'approvals': [
+ {'approval': 'UXReview',
+ 'status': 'APPROVED',
+ 'setter': 'user1@test.com',
+ 'set_on': 7,
+ 'approvers': ['user3@test.com', 'user4@test.com'],
+ 'phase': 'Phase1'}
+ ],
+ 'starrers': ['user2@test.com', 'user3@test.com'],
+ 'comments': [
+ {'content': 'simple',
+ 'timestamp': None,
+ 'amendments': [],
+ 'commenter': None,
+ 'attachments': [],
+ 'description_num': None},
+ {'content': 'issue desc',
+ 'timestamp': None,
+ 'amendments': [],
+ 'commenter': None,
+ 'attachments': [],
+ 'description_num': '1'},
+ ],
+ 'opened': 1,
+ 'modified': 3,
+ 'closed': 2,
+ }
+
+ self.assertEqual(expected_JSON, issue_JSON)
diff --git a/tracker/test/issueimport_test.py b/tracker/test/issueimport_test.py
new file mode 100644
index 0000000..c0e38af
--- /dev/null
+++ b/tracker/test/issueimport_test.py
@@ -0,0 +1,69 @@
+# Copyright 2016 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
+
+"""Unittests for the issueimport servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from framework import permissions
+from services import service_manager
+from testing import testing_helpers
+from tracker import issueimport
+from proto import tracker_pb2
+
+
+class IssueExportTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services()
+ self.servlet = issueimport.IssueImport(
+ 'req', 'res', services=self.services)
+ self.event_log = None
+
+ def testAssertBasePermission(self):
+ """Only site admins can import issues."""
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.OWNER_ACTIVE_PERMISSIONSET)
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+ mr.auth.user_pb.is_site_admin = True
+ self.servlet.AssertBasePermission(mr)
+
+ def testParseComment(self):
+ """Test a Comment JSON is correctly parsed."""
+ users_id_dict = {'adam@test.com': 111}
+ json = {
+ 'timestamp': 123,
+ 'commenter': 'adam@test.com',
+ 'content': 'so basically, what I was thinkig of',
+ 'amendments': [],
+ 'attachments': [],
+ 'description_num': None,
+ }
+ comment = self.servlet._ParseComment(
+ 12, users_id_dict, json, self.event_log)
+ self.assertEqual(
+ comment, tracker_pb2.IssueComment(
+ project_id=12, timestamp=123, user_id=111,
+ content='so basically, what I was thinkig of'))
+
+ json_desc = {
+ 'timestamp': 223,
+ 'commenter': 'adam@test.com',
+ 'content': 'I cant believe youve done this',
+ 'description_num': '2',
+ 'amendments': [],
+ 'attachments': [],
+ }
+ desc_comment = self.servlet._ParseComment(
+ 12, users_id_dict, json_desc, self.event_log)
+ self.assertEqual(
+ desc_comment, tracker_pb2.IssueComment(
+ project_id=12, timestamp=223, user_id=111,
+ content='I cant believe youve done this',
+ is_description=True))
diff --git a/tracker/test/issueoriginal_test.py b/tracker/test/issueoriginal_test.py
new file mode 100644
index 0000000..1b2b7d6
--- /dev/null
+++ b/tracker/test/issueoriginal_test.py
@@ -0,0 +1,226 @@
+# Copyright 2016 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 issueoriginal module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mock
+import unittest
+
+import webapp2
+
+from framework import exceptions
+from framework import framework_helpers
+from framework import monorailrequest
+from framework import permissions
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import issueoriginal
+
+
+STRIPPED_MSG = 'Are you sure that it is plugged in?\n'
+ORIG_MSG = ('Are you sure that it is plugged in?\n'
+ '\n'
+ '> Issue 1 entered by user foo:\n'
+ '> http://blah blah\n'
+ '> The screen is just dark when I press power on\n')
+XXX_GOOD_UNICODE_MSG = u'Thanks,\n\342\230\206*username*'.encode('utf-8')
+GOOD_UNICODE_MSG = u'Thanks,\n XXX *username*'
+XXX_BAD_UNICODE_MSG = ORIG_MSG + ('\xff' * 1000)
+BAD_UNICODE_MSG = ORIG_MSG + 'XXX'
+GMAIL_CRUFT_MSG = ORIG_MSG # XXX .replace(' ', ' \xa0 ')
+
+
+class IssueOriginalTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService())
+ self.servlet = issueoriginal.IssueOriginal(
+ 'req', 'res', services=self.services)
+
+ self.proj = self.services.project.TestAddProject('proj', project_id=789)
+ summary = 'System wont boot'
+ status = 'New'
+ cnxn = 'fake connection'
+ self.services.user.TestAddUser('commenter@example.com', 222)
+
+ created_issue_1 = fake.MakeTestIssue(
+ 789, 1, summary, status, 111, reporter_id=111)
+ self.services.issue.TestAddIssue(created_issue_1)
+ self.local_id_1 = created_issue_1.local_id
+ comment_0 = tracker_pb2.IssueComment(
+ issue_id=created_issue_1.issue_id,
+ user_id=222,
+ project_id=789,
+ content=STRIPPED_MSG,
+ inbound_message=ORIG_MSG)
+ self.services.issue.InsertComment(cnxn, comment_0)
+ comment_1 = tracker_pb2.IssueComment(
+ issue_id=created_issue_1.issue_id,
+ user_id=222,
+ project_id=789,
+ content=STRIPPED_MSG,
+ inbound_message=BAD_UNICODE_MSG)
+ self.services.issue.InsertComment(cnxn, comment_1)
+ comment_2 = tracker_pb2.IssueComment(
+ issue_id=created_issue_1.issue_id,
+ user_id=222,
+ project_id=789,
+ content=STRIPPED_MSG,
+ inbound_message=GMAIL_CRUFT_MSG)
+ self.services.issue.InsertComment(cnxn, comment_2)
+ comment_3 = tracker_pb2.IssueComment(
+ issue_id=created_issue_1.issue_id,
+ user_id=222,
+ project_id=789,
+ content=STRIPPED_MSG,
+ inbound_message=GOOD_UNICODE_MSG)
+ self.services.issue.InsertComment(cnxn, comment_3)
+ self.issue_1 = self.services.issue.GetIssueByLocalID(
+ cnxn, 789, self.local_id_1)
+ self.comments = [comment_0, comment_1, comment_2, comment_3]
+
+ @mock.patch('framework.permissions.GetPermissions')
+ def testAssertBasePermission(self, mock_getpermissions):
+ """Permit users who can view issue, view inbound message and delete."""
+ _request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj/issues/original?id=1&seq=1',
+ project=self.proj)
+
+ # Allow the user to view the issue itself.
+ mock_getpermissions.return_value = (
+ permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET)
+
+ # Someone without VIEW permission cannot view the inbound email.
+ mr.perms = permissions.EMPTY_PERMISSIONSET
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ # Contributors don't have VIEW_INBOUND_MESSAGES.
+ mr.perms = permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ # Committers do have VIEW_INBOUND_MESSAGES.
+ mr.perms = permissions.COMMITTER_ACTIVE_PERMISSIONSET
+ self.servlet.AssertBasePermission(mr)
+
+ # But, a committer cannot use that if they cannot view the issue.
+ self.issue_1.labels.append('Restrict-View-Foo')
+ mr.perms = permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ # Project owners have VIEW_INBOUND_MESSAGES and bypass restrictions.
+ mock_getpermissions.return_value = (
+ permissions.OWNER_ACTIVE_PERMISSIONSET)
+ mr.perms = permissions.OWNER_ACTIVE_PERMISSIONSET
+ self.servlet.AssertBasePermission(mr)
+
+ def testGatherPageData_Normal(self):
+ _request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj/issues/original?id=1&seq=1',
+ project=self.proj)
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(1, page_data['local_id'])
+ self.assertEqual(1, page_data['seq'])
+ self.assertFalse(page_data['is_binary'])
+ self.assertEqual(ORIG_MSG, page_data['message_body'])
+
+ def testGatherPageData_GoodUnicode(self):
+ _request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj/issues/original?id=1&seq=4',
+ project=self.proj)
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(1, page_data['local_id'])
+ self.assertEqual(4, page_data['seq'])
+ self.assertEqual(GOOD_UNICODE_MSG, page_data['message_body'])
+ self.assertFalse(page_data['is_binary'])
+
+ def testGatherPageData_BadUnicode(self):
+ _request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj/issues/original?id=1&seq=2',
+ project=self.proj)
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(1, page_data['local_id'])
+ self.assertEqual(2, page_data['seq'])
+ # xxx: should be true if cruft was there.
+ # self.assertTrue(page_data['is_binary'])
+
+ def testGatherPageData_GmailCruft(self):
+ _request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj/issues/original?id=1&seq=3',
+ project=self.proj)
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(1, page_data['local_id'])
+ self.assertEqual(3, page_data['seq'])
+ self.assertFalse(page_data['is_binary'])
+ self.assertEqual(ORIG_MSG, page_data['message_body'])
+
+ def testGatherPageData_404(self):
+ _request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj/issues/original',
+ project=self.proj)
+ with self.assertRaises(webapp2.HTTPException) as cm:
+ self.servlet.GatherPageData(mr)
+ self.assertEqual(404, cm.exception.code)
+
+ _request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj/issues/original?id=1&seq=999',
+ project=self.proj)
+ with self.assertRaises(webapp2.HTTPException) as cm:
+ self.servlet.GatherPageData(mr)
+ self.assertEqual(404, cm.exception.code)
+
+ _request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj/issues/original?id=999&seq=1',
+ project=self.proj)
+ with self.assertRaises(exceptions.NoSuchIssueException) as cm:
+ self.servlet.GatherPageData(mr)
+
+ def testGetIssueAndComment_Normal(self):
+ _request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj/issues/original?id=1&seq=1',
+ project=self.proj)
+ issue, comment = self.servlet._GetIssueAndComment(mr)
+ self.assertEqual(self.issue_1, issue)
+ self.assertEqual(self.comments[1].content, comment.content)
+
+ def testGetIssueAndComment_NoSuchComment(self):
+ _request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj/issues/original?id=1&seq=99',
+ project=self.proj)
+ with self.assertRaises(webapp2.HTTPException) as cm:
+ self.servlet._GetIssueAndComment(mr)
+ self.assertEqual(404, cm.exception.code)
+
+ def testGetIssueAndComment_Malformed(self):
+ _request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj/issues/original',
+ project=self.proj)
+ with self.assertRaises(webapp2.HTTPException) as cm:
+ self.servlet._GetIssueAndComment(mr)
+ self.assertEqual(404, cm.exception.code)
+
+ _request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj/issues/original?id=1',
+ project=self.proj)
+ with self.assertRaises(webapp2.HTTPException) as cm:
+ self.servlet._GetIssueAndComment(mr)
+ self.assertEqual(404, cm.exception.code)
+
+ _request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj/issues/original?seq=1',
+ project=self.proj)
+ with self.assertRaises(exceptions.NoSuchIssueException) as cm:
+ self.servlet._GetIssueAndComment(mr)
diff --git a/tracker/test/issuereindex_test.py b/tracker/test/issuereindex_test.py
new file mode 100644
index 0000000..fe033b8
--- /dev/null
+++ b/tracker/test/issuereindex_test.py
@@ -0,0 +1,124 @@
+# Copyright 2016 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
+
+"""Unittests for monorail.tracker.issuereindex."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import mox
+
+import settings
+from framework import permissions
+from framework import template_helpers
+from services import service_manager
+from services import tracker_fulltext
+from testing import fake
+from testing import testing_helpers
+from tracker import issuereindex
+
+
+class IssueReindexTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ project=fake.ProjectService())
+ self.project = self.services.project.TestAddProject('proj', project_id=987)
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testAssertBasePermission_NoAccess(self):
+ # Non-members and contributors do not have permission to view this page.
+ for permission in (permissions.USER_PERMISSIONSET,
+ permissions.COMMITTER_ACTIVE_PERMISSIONSET):
+ request, mr = testing_helpers.GetRequestObjects(
+ project=self.project, perms=permission)
+ servlet = issuereindex.IssueReindex(
+ request, 'res', services=self.services)
+ with self.assertRaises(permissions.PermissionException) as cm:
+ servlet.AssertBasePermission(mr)
+ self.assertEqual('You are not allowed to administer this project',
+ cm.exception.message)
+
+ def testAssertBasePermission_WithAccess(self):
+ # Owners and admins have permission to view this page.
+ for permission in (permissions.OWNER_ACTIVE_PERMISSIONSET,
+ permissions.ADMIN_PERMISSIONSET):
+ request, mr = testing_helpers.GetRequestObjects(
+ project=self.project, perms=permission)
+ servlet = issuereindex.IssueReindex(
+ request, 'res', services=self.services)
+ servlet.AssertBasePermission(mr)
+
+ def testGatherPageData(self):
+ servlet = issuereindex.IssueReindex('req', 'res', services=self.services)
+
+ mr = testing_helpers.MakeMonorailRequest()
+ mr.auto_submit = True
+ ret = servlet.GatherPageData(mr)
+
+ self.assertTrue(ret['auto_submit'])
+ self.assertIsNone(ret['issue_tab_mode'])
+ self.assertTrue(ret['page_perms'].CreateIssue)
+
+ def _callProcessFormData(self, post_data, index_issue_1=True):
+ servlet = issuereindex.IssueReindex('req', 'res', services=self.services)
+
+ mr = testing_helpers.MakeMonorailRequest(project=self.project)
+ mr.cnxn = self.cnxn
+
+ issue1 = fake.MakeTestIssue(
+ project_id=self.project.project_id, local_id=1, summary='sum',
+ status='New', owner_id=111)
+ issue1.project_name = self.project.project_name
+ self.services.issue.TestAddIssue(issue1)
+
+ self.mox.StubOutWithMock(tracker_fulltext, 'IndexIssues')
+ if index_issue_1:
+ tracker_fulltext.IndexIssues(
+ self.cnxn, [issue1], self.services.user, self.services.issue,
+ self.services.config)
+
+ self.mox.ReplayAll()
+
+ ret = servlet.ProcessFormData(mr, post_data)
+ self.mox.VerifyAll()
+ return ret
+
+ def testProcessFormData_NormalInputs(self):
+ post_data = {'start': 1, 'num': 5}
+ ret = self._callProcessFormData(post_data)
+ self.assertEqual(
+ '/p/None/issues/reindex?start=6&auto_submit=False&num=5', ret)
+
+ def testProcessFormData_LargeInputs(self):
+ post_data = {'start': 0, 'num': 10000000}
+ ret = self._callProcessFormData(post_data)
+ self.assertEqual(
+ '/p/None/issues/reindex?start=%s&auto_submit=False&num=%s' % (
+ settings.max_artifact_search_results_per_page,
+ settings.max_artifact_search_results_per_page), ret)
+
+ def testProcessFormData_WithAutoSubmit(self):
+ post_data = {'start': 1, 'num': 5, 'auto_submit': 1}
+ ret = self._callProcessFormData(post_data)
+ self.assertEqual(
+ '/p/None/issues/reindex?start=6&auto_submit=True&num=5', ret)
+
+ def testProcessFormData_WithAutoSubmitButNoMoreIssues(self):
+ """This project has no issues 6-10, so stop autosubmitting."""
+ post_data = {'start': 6, 'num': 5, 'auto_submit': 1}
+ ret = self._callProcessFormData(post_data, index_issue_1=False)
+ self.assertEqual(
+ '/p/None/issues/reindex?start=11&auto_submit=False&num=5', ret)
diff --git a/tracker/test/issuetips_test.py b/tracker/test/issuetips_test.py
new file mode 100644
index 0000000..44f5f70
--- /dev/null
+++ b/tracker/test/issuetips_test.py
@@ -0,0 +1,33 @@
+# Copyright 2016 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 issuetips module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import issuetips
+
+
+class IssueTipsTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ project=fake.ProjectService())
+ self.servlet = issuetips.IssueSearchTips(
+ 'req', 'res', services=self.services)
+
+ def testGatherPageData(self):
+ mr = testing_helpers.MakeMonorailRequest(path='/p/proj/issues/tips')
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual('issueSearchTips', page_data['issue_tab_mode'])
diff --git a/tracker/test/rerank_helpers_test.py b/tracker/test/rerank_helpers_test.py
new file mode 100644
index 0000000..47ddd47
--- /dev/null
+++ b/tracker/test/rerank_helpers_test.py
@@ -0,0 +1,135 @@
+# Copyright 2016 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
+
+"""Unittests for monorail.tracker.rerank_helpers."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from framework import exceptions
+from testing import fake
+from tracker import rerank_helpers
+
+
+rerank_helpers.MAX_RANKING = 10
+
+
+class Rerank_HelpersTest(unittest.TestCase):
+
+ def setUp(self):
+ self.PAST_TIME = 12345
+ hotlist_item_fields = [
+ (78904, 31, 111, self.PAST_TIME, 'note'),
+ (78903, 21, 222, self.PAST_TIME, 'note'),
+ (78902, 11, 111, self.PAST_TIME, 'note'),
+ (78901, 1, 222, self.PAST_TIME, 'note')]
+ self.hotlist = fake.Hotlist(
+ 'hotlist_name', 1234, hotlist_item_fields=hotlist_item_fields)
+
+ # Tested in tests for RerankHotlistItems.
+ def testGetHotlistRerankChanges_FirstPosition(self):
+ moved_issue_ids = [78903, 78902]
+ target_position = 0
+ changed_ranks = rerank_helpers.GetHotlistRerankChanges(
+ self.hotlist.items, moved_issue_ids, target_position)
+ self.assertEqual(changed_ranks, [(78903, 5), (78902, 15), (78901, 25)])
+
+ def testGetHotlistRerankChanges_LastPosition(self):
+ moved_issue_ids = [78903, 78902]
+ target_position = 2
+ changed_ranks = rerank_helpers.GetHotlistRerankChanges(
+ self.hotlist.items, moved_issue_ids, target_position)
+ self.assertEqual(changed_ranks, [(78904, 3), (78903, 6), (78902, 9)])
+
+ def testGetHotlistRerankChanges_Middle(self):
+ moved_issue_ids = [78903]
+ target_position = 1
+ changed_ranks = rerank_helpers.GetHotlistRerankChanges(
+ self.hotlist.items, moved_issue_ids, target_position)
+ self.assertEqual(changed_ranks, [(78903, 6)])
+
+
+ def testGetHotlistRerankChanges_NewMoveIds(self):
+ "We can handle reranking for inserting new issues."
+ moved_issue_ids = [78909, 78910, 78903]
+ target_position = 0
+ changed_ranks = rerank_helpers.GetHotlistRerankChanges(
+ self.hotlist.items, moved_issue_ids, target_position)
+ self.assertEqual(
+ changed_ranks, [(78909, 1), (78910, 3), (78903, 5), (78901, 7)])
+
+ def testGetHotlistRerankChanges_InvalidMovedIds(self):
+ moved_issue_ids = [78903]
+ target_position = -1
+ with self.assertRaises(exceptions.InputException):
+ rerank_helpers.GetHotlistRerankChanges(
+ self.hotlist.items, moved_issue_ids, target_position)
+
+ def testGetHotlistRerankChanges_InvalidPosition(self):
+ moved_issue_ids = [78909]
+ target_position = 8
+ with self.assertRaises(exceptions.InputException):
+ rerank_helpers.GetHotlistRerankChanges(
+ self.hotlist.items, moved_issue_ids, target_position)
+
+ def testGetInsertRankings(self):
+ lower = [(1, 0)]
+ higher = [(2, 10)]
+ moved_ids = [3]
+ ret = rerank_helpers.GetInsertRankings(lower, higher, moved_ids)
+ self.assertEqual(ret, [(3, 5)])
+
+ def testGetInsertRankings_Below(self):
+ lower = []
+ higher = [(1, 2)]
+ moved_ids = [2]
+ ret = rerank_helpers.GetInsertRankings(lower, higher, moved_ids)
+ self.assertEqual(ret, [(2, 1)])
+
+ def testGetInsertRankings_Above(self):
+ lower = [(1, 0)]
+ higher = []
+ moved_ids = [2]
+ ret = rerank_helpers.GetInsertRankings(lower, higher, moved_ids)
+ self.assertEqual(ret, [(2, 5)])
+
+ def testGetInsertRankings_Multiple(self):
+ lower = [(1, 0)]
+ higher = [(2, 10)]
+ moved_ids = [3,4,5]
+ ret = rerank_helpers.GetInsertRankings(lower, higher, moved_ids)
+ self.assertEqual(ret, [(3, 2), (4, 5), (5, 8)])
+
+ def testGetInsertRankings_SplitLow(self):
+ lower = [(1, 0), (2, 5)]
+ higher = [(3, 6), (4, 10)]
+ moved_ids = [5]
+ ret = rerank_helpers.GetInsertRankings(lower, higher, moved_ids)
+ self.assertEqual(ret, [(2, 2), (5, 5)])
+
+ def testGetInsertRankings_SplitHigh(self):
+ lower = [(1, 0), (2, 4)]
+ higher = [(3, 5), (4, 10)]
+ moved_ids = [5]
+ ret = rerank_helpers.GetInsertRankings(lower, higher, moved_ids)
+ self.assertEqual(ret, [(5, 6), (3, 9)])
+
+ def testGetInsertRankings_NoLower(self):
+ lower = []
+ higher = [(1, 1)]
+ moved_ids = [2]
+ ret = rerank_helpers.GetInsertRankings(lower, higher, moved_ids)
+ self.assertEqual(ret, [(2, 3), (1, 8)])
+
+ def testGetInsertRankings_NoRoom(self):
+ max_ranking, rerank_helpers.MAX_RANKING = rerank_helpers.MAX_RANKING, 1
+ lower = [(1, 0)]
+ higher = [(2, 1)]
+ moved_ids = [3]
+ ret = rerank_helpers.GetInsertRankings(lower, higher, moved_ids)
+ self.assertIsNone(ret)
+ rerank_helpers.MAX_RANKING = max_ranking
diff --git a/tracker/test/tablecell_test.py b/tracker/test/tablecell_test.py
new file mode 100644
index 0000000..c8b7292
--- /dev/null
+++ b/tracker/test/tablecell_test.py
@@ -0,0 +1,491 @@
+# Copyright 2016 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
+
+"""Unit tests for issuelist module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import time
+import unittest
+
+from framework import framework_constants
+from framework import table_view_helpers
+from framework import template_helpers
+from proto import tracker_pb2
+from testing import fake
+from testing import testing_helpers
+from tracker import tablecell
+from tracker import tracker_bizobj
+
+
+class DisplayNameMock(object):
+
+ def __init__(self, name):
+ self.display_name = name
+ self.user = None
+
+
+def MakeTestIssue(local_id, issue_id, summary, status=None):
+ issue = tracker_pb2.Issue()
+ issue.local_id = local_id
+ issue.issue_id = issue_id
+ issue.summary = summary
+ if status:
+ issue.status = status
+ return issue
+
+
+class TableCellUnitTest(unittest.TestCase):
+
+ USERS_BY_ID = {
+ 23456: DisplayNameMock('Jason'),
+ 34567: DisplayNameMock('Nathan'),
+ }
+
+ def setUp(self):
+ self.issue1 = MakeTestIssue(
+ local_id=1, issue_id=100001, summary='One', status="New")
+ self.issue2 = MakeTestIssue(
+ local_id=2, issue_id=100002, summary='Two', status="Fixed")
+ self.issue3 = MakeTestIssue(
+ local_id=3, issue_id=100003, summary='Three', status="UndefinedString")
+ self.issue5 = MakeTestIssue(
+ local_id=5, issue_id=100005, summary='FiveUnviewable', status="Fixed")
+ self.table_cell_kws = {
+ 'col': None,
+ 'users_by_id': self.USERS_BY_ID,
+ 'non_col_labels': [],
+ 'label_values': {},
+ 'related_issues': {},
+ 'config': tracker_bizobj.MakeDefaultProjectIssueConfig(678),
+ 'viewable_iids_set': {100001, 100002, 100003}
+ }
+
+ def testTableCellNote(self):
+ table_cell_kws = self.table_cell_kws.copy()
+ table_cell_kws.update({'note': ''})
+ cell = tablecell.TableCellNote(
+ self.issue1, **table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_NOTE)
+ self.assertEqual(cell.values, [])
+
+ def testTableCellNote_NoNote(self):
+ table_cell_kws = self.table_cell_kws.copy()
+ table_cell_kws.update({'note': 'some note'})
+ cell = tablecell.TableCellNote(
+ self.issue1, **table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_NOTE)
+ self.assertEqual(cell.values[0].item, 'some note')
+
+ def testTableCellDateAdded(self):
+ table_cell_kws = self.table_cell_kws.copy()
+ table_cell_kws.update({'date_added': 1234})
+ cell = tablecell.TableCellDateAdded(
+ self.issue1, **table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ATTR)
+ self.assertEqual(cell.values[0].item, 1234)
+
+ def testTableCellAdderID(self):
+ table_cell_kws = self.table_cell_kws.copy()
+ table_cell_kws.update({'adder_id': 23456})
+ cell = tablecell.TableCellAdderID(
+ self.issue1, **table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ATTR)
+ self.assertEqual(cell.values[0].item, 'Jason')
+
+ def testTableCellRank(self):
+ table_cell_kws = self.table_cell_kws.copy()
+ table_cell_kws.update({'issue_rank': 3})
+ cell = tablecell.TableCellRank(
+ self.issue1, **table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ATTR)
+ self.assertEqual(cell.values[0].item, 3)
+
+ def testTableCellID(self):
+ cell = tablecell.TableCellID(
+ MakeTestIssue(4, 4, 'Four'), **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ID)
+ # Note that the ID itself is accessed from the row, not the cell.
+
+ def testTableCellOwner(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.owner_id=23456
+
+ cell = tablecell.TableCellOwner(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ATTR)
+ self.assertEqual(cell.values[0].item, 'Jason')
+
+ def testTableCellOwnerNoOwner(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.owner_id=framework_constants.NO_USER_SPECIFIED
+
+ cell = tablecell.TableCellOwner(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ATTR)
+ self.assertEqual(cell.values, [])
+
+ def testTableCellReporter(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.reporter_id=34567
+
+ cell = tablecell.TableCellReporter(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ATTR)
+ self.assertEqual(cell.values[0].item, 'Nathan')
+
+ def testTableCellCc(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.cc_ids = [23456, 34567]
+
+ cell = tablecell.TableCellCc(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ATTR)
+ self.assertEqual(cell.values[0].item, 'Jason')
+ self.assertEqual(cell.values[1].item, 'Nathan')
+
+ def testTableCellCcNoCcs(self):
+ cell = tablecell.TableCellCc(
+ MakeTestIssue(4, 4, 'Four'), **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ATTR)
+ self.assertEqual(cell.values, [])
+
+ def testTableCellAttachmentsNone(self):
+ cell = tablecell.TableCellAttachments(
+ MakeTestIssue(4, 4, 'Four'), **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ATTR)
+ self.assertEqual(cell.values[0].item, 0)
+
+ def testTableCellAttachments(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.attachment_count = 2
+
+ cell = tablecell.TableCellAttachments(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ATTR)
+ self.assertEqual(cell.values[0].item, 2)
+
+ def testTableCellOpened(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.opened_timestamp = 1200000000
+
+ cell = tablecell.TableCellOpened(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 'Jan 2008')
+
+ def testTableCellClosed(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.closed_timestamp = None
+
+ cell = tablecell.TableCellClosed(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values, [])
+
+ test_issue.closed_timestamp = 1200000000
+ cell = tablecell.TableCellClosed(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 'Jan 2008')
+
+ def testTableCellModified(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.modified_timestamp = None
+
+ cell = tablecell.TableCellModified(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values, [])
+
+ test_issue.modified_timestamp = 1200000000
+ cell = tablecell.TableCellModified(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 'Jan 2008')
+
+ def testTableCellOwnerLastVisit(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.owner_id = None
+
+ cell = tablecell.TableCellOwnerLastVisit(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values, [])
+
+ test_issue.owner_id = 23456
+ self.USERS_BY_ID[23456].user = testing_helpers.Blank(last_visit_timestamp=0)
+ cell = tablecell.TableCellOwnerLastVisit(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values, [])
+
+ self.USERS_BY_ID[23456].user.last_visit_timestamp = int(time.time())
+ cell = tablecell.TableCellOwnerLastVisit(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 'Today')
+
+ self.USERS_BY_ID[23456].user.last_visit_timestamp = (
+ int(time.time()) - 25 * framework_constants.SECS_PER_HOUR)
+ cell = tablecell.TableCellOwnerLastVisit(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 'Yesterday')
+
+ def testTableCellBlockedOn(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.blocked_on_iids = [
+ self.issue1.issue_id, self.issue2.issue_id, self.issue3.issue_id,
+ self.issue5.issue_id]
+ table_cell_kws = self.table_cell_kws.copy()
+ table_cell_kws['related_issues'] = {
+ self.issue1.issue_id: self.issue1, self.issue2.issue_id: self.issue2,
+ self.issue3.issue_id: self.issue3, self.issue5.issue_id: self.issue5}
+
+ cell = tablecell.TableCellBlockedOn(
+ test_issue, **table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ISSUES)
+ self.assertEqual(
+ [x.item for x in cell.values], [
+ template_helpers.EZTItem(
+ href='/p/None/issues/detail?id=1',
+ id='1',
+ closed=None,
+ title='One'),
+ template_helpers.EZTItem(
+ href='/p/None/issues/detail?id=3',
+ id='3',
+ closed=None,
+ title='Three'),
+ template_helpers.EZTItem(
+ href='/p/None/issues/detail?id=5',
+ id='5',
+ closed=None,
+ title=''),
+ template_helpers.EZTItem(
+ href='/p/None/issues/detail?id=2',
+ id='2',
+ closed='yes',
+ title='Two')
+ ])
+
+ def testTableCellBlockedOnNone(self):
+ cell = tablecell.TableCellBlockedOn(
+ MakeTestIssue(4, 4, 'Four'), **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ISSUES)
+ self.assertEqual(cell.values, [])
+
+ def testTableCellBlocking(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.blocking_iids = [
+ self.issue1.issue_id, self.issue2.issue_id, self.issue3.issue_id,
+ self.issue5.issue_id]
+ table_cell_kws = self.table_cell_kws.copy()
+ table_cell_kws['related_issues'] = {
+ self.issue1.issue_id: self.issue1, self.issue2.issue_id: self.issue2,
+ self.issue3.issue_id: self.issue3, self.issue5.issue_id: self.issue5}
+
+ cell = tablecell.TableCellBlocking(
+ test_issue, **table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ISSUES)
+ self.assertEqual(
+ [x.item for x in cell.values], [
+ template_helpers.EZTItem(
+ href='/p/None/issues/detail?id=1',
+ id='1',
+ closed=None,
+ title='One'),
+ template_helpers.EZTItem(
+ href='/p/None/issues/detail?id=3',
+ id='3',
+ closed=None,
+ title='Three'),
+ template_helpers.EZTItem(
+ href='/p/None/issues/detail?id=5',
+ id='5',
+ closed=None,
+ title=''),
+ template_helpers.EZTItem(
+ href='/p/None/issues/detail?id=2',
+ id='2',
+ closed='yes',
+ title='Two')
+ ])
+
+ def testTableCellBlockingNone(self):
+ cell = tablecell.TableCellBlocking(
+ MakeTestIssue(4, 4, 'Four'),
+ **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ISSUES)
+ self.assertEqual(cell.values, [])
+
+ def testTableCellBlocked(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.blocked_on_iids = [1, 2, 3]
+
+ cell = tablecell.TableCellBlocked(
+ test_issue, **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ATTR)
+ self.assertEqual(cell.values[0].item, 'Yes')
+
+ def testTableCellBlockedNotBlocked(self):
+ cell = tablecell.TableCellBlocked(
+ MakeTestIssue(4, 4, 'Four'), **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ATTR)
+ self.assertEqual(cell.values[0].item, 'No')
+
+ def testTableCellMergedInto(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.merged_into = self.issue2.issue_id
+ table_cell_kws = self.table_cell_kws.copy()
+ table_cell_kws['related_issues'] = {self.issue2.issue_id: self.issue2}
+
+ cell = tablecell.TableCellMergedInto(
+ test_issue, **table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ISSUES)
+ self.assertEqual(
+ cell.values[0].item,
+ template_helpers.EZTItem(
+ href='/p/None/issues/detail?id=2',
+ id='2',
+ closed='yes',
+ title='Two'))
+
+ def testTableCellMergedIntoUnviewable(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.merged_into = self.issue5.issue_id
+ table_cell_kws = self.table_cell_kws.copy()
+ table_cell_kws['related_issues'] = {self.issue5.issue_id: self.issue5}
+
+ cell = tablecell.TableCellMergedInto(
+ test_issue, **table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ISSUES)
+ self.assertEqual(
+ cell.values[0].item,
+ template_helpers.EZTItem(
+ href='/p/None/issues/detail?id=5', id='5', closed=None, title=''))
+
+ def testTableCellMergedIntoNotMerged(self):
+ cell = tablecell.TableCellMergedInto(
+ MakeTestIssue(4, 4, 'Four'), **self.table_cell_kws)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ISSUES)
+ self.assertEqual(cell.values, [])
+
+ def testTableCellAllLabels(self):
+ labels = ['A', 'B', 'C', 'D-E', 'F-G']
+ derived_labels = ['W', 'X', 'Y-Z']
+
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.labels = labels
+ test_issue.derived_labels = derived_labels
+
+ cell = tablecell.TableCellAllLabels(test_issue)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ATTR)
+ self.assertEqual([v.item for v in cell.values], labels + derived_labels)
+
+
+class TableCellCSVTest(unittest.TestCase):
+
+ USERS_BY_ID = {
+ 23456: DisplayNameMock('Jason'),
+ }
+
+ def testTableCellOpenedTimestamp(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.opened_timestamp = 1200000000
+
+ cell = tablecell.TableCellOpenedTimestamp(test_issue)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 1200000000)
+
+ def testTableCellClosedTimestamp(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.closed_timestamp = None
+
+ cell = tablecell.TableCellClosedTimestamp(test_issue)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 0)
+
+ test_issue.closed_timestamp = 1200000000
+ cell = tablecell.TableCellClosedTimestamp(test_issue)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 1200000000)
+
+ def testTableCellModifiedTimestamp(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.modified_timestamp = 0
+
+ cell = tablecell.TableCellModifiedTimestamp(test_issue)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 0)
+
+ test_issue.modified_timestamp = 1200000000
+ cell = tablecell.TableCellModifiedTimestamp(test_issue)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 1200000000)
+
+ def testTableCellOwnerModifiedTimestamp(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.owner_modified_timestamp = 0
+
+ cell = tablecell.TableCellOwnerModifiedTimestamp(test_issue)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 0)
+
+ test_issue.owner_modified_timestamp = 1200000000
+ cell = tablecell.TableCellOwnerModifiedTimestamp(test_issue)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 1200000000)
+
+ def testTableCellStatusModifiedTimestamp(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.status_modified_timestamp = 0
+
+ cell = tablecell.TableCellStatusModifiedTimestamp(test_issue)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 0)
+
+ test_issue.status_modified_timestamp = 1200000000
+ cell = tablecell.TableCellStatusModifiedTimestamp(test_issue)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 1200000000)
+
+ def testTableCellComponentModifiedTimestamp(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.component_modified_timestamp = 0
+
+ cell = tablecell.TableCellComponentModifiedTimestamp(test_issue)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 0)
+
+ test_issue.component_modified_timestamp = 1200000000
+ cell = tablecell.TableCellComponentModifiedTimestamp(test_issue)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(cell.values[0].item, 1200000000)
+
+ def testTableCellOwnerLastVisitDaysAgo(self):
+ test_issue = MakeTestIssue(4, 4, 'Four')
+ test_issue.owner_id = None
+
+ cell = tablecell.TableCellOwnerLastVisitDaysAgo(
+ test_issue, users_by_id=self.USERS_BY_ID)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(None, cell.values[0].item)
+
+ test_issue.owner_id = 23456
+ self.USERS_BY_ID[23456].user = testing_helpers.Blank(last_visit_timestamp=0)
+ cell = tablecell.TableCellOwnerLastVisitDaysAgo(
+ test_issue, users_by_id=self.USERS_BY_ID)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(None, cell.values[0].item)
+
+ self.USERS_BY_ID[23456].user.last_visit_timestamp = (
+ int(time.time()) - 25 * 60 * 60)
+ cell = tablecell.TableCellOwnerLastVisitDaysAgo(
+ test_issue, users_by_id=self.USERS_BY_ID)
+ self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_UNFILTERABLE)
+ self.assertEqual(1, cell.values[0].item)
diff --git a/tracker/test/template_helpers_test.py b/tracker/test/template_helpers_test.py
new file mode 100644
index 0000000..6c4a034
--- /dev/null
+++ b/tracker/test/template_helpers_test.py
@@ -0,0 +1,355 @@
+# 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
+
+"""Unittest for the template helpers module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import unittest
+
+import settings
+
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import template_helpers
+from tracker import tracker_bizobj
+from proto import tracker_pb2
+
+
+class TemplateHelpers(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ config=fake.ConfigService(),
+ project=fake.ProjectService(),
+ usergroup=fake.UserGroupService())
+ self.project = self.services.project.TestAddProject('proj')
+ self.mr = testing_helpers.MakeMonorailRequest(
+ project=self.project)
+ self.config = self.services.config.GetProjectConfig(
+ 'fake cnxn', self.project.project_id)
+ self.fd_1 = tracker_bizobj.MakeFieldDef(
+ 1, 789, 'UXReview', tracker_pb2.FieldTypes.STR_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'Approval for UX review', False)
+ self.fd_2 = tracker_bizobj.MakeFieldDef(
+ 2, 789, 'UXReview', tracker_pb2.FieldTypes.STR_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'Approval for UX review', False)
+ self.fd_3 = tracker_bizobj.MakeFieldDef(
+ 3, 789, 'UXApproval', tracker_pb2.FieldTypes.APPROVAL_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'Approval for UX review', False)
+ self.fd_4 = tracker_bizobj.MakeFieldDef(
+ 4, 789, 'TestApproval', tracker_pb2.FieldTypes.APPROVAL_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'Approval for Test review', False)
+ self.fd_5 = tracker_bizobj.MakeFieldDef(
+ 5, 789, 'SomeApproval', tracker_pb2.FieldTypes.APPROVAL_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'Approval for Test review', False)
+ self.ad_3 = tracker_pb2.ApprovalDef(approval_id=3)
+ self.ad_4 = tracker_pb2.ApprovalDef(approval_id=4)
+ self.ad_5 = tracker_pb2.ApprovalDef(approval_id=5)
+ self.cd_1 = tracker_bizobj.MakeComponentDef(
+ 1, 789, 'BackEnd', 'doc', False, [111], [], 100000, 222)
+
+ self.services.user.TestAddUser('1@ex.com', 111)
+ self.services.user.TestAddUser('2@ex.com', 222)
+ self.services.user.TestAddUser('3@ex.com', 333)
+ self.services.project.TestAddProjectMembers(
+ [111], self.project, 'OWNER_ROLE')
+
+ def testParseTemplateRequest_Empty(self):
+ post_data = fake.PostData()
+ parsed = template_helpers.ParseTemplateRequest(post_data, self.config)
+ self.assertEqual(parsed.name, '')
+ self.assertFalse(parsed.members_only)
+ self.assertEqual(parsed.summary, '')
+ self.assertFalse(parsed.summary_must_be_edited)
+ self.assertEqual(parsed.content, '')
+ self.assertEqual(parsed.status, '')
+ self.assertEqual(parsed.owner_str, '')
+ self.assertEqual(parsed.labels, [])
+ self.assertEqual(parsed.field_val_strs, {})
+ self.assertEqual(parsed.component_paths, [])
+ self.assertFalse(parsed.component_required)
+ self.assertFalse(parsed.owner_defaults_to_member)
+ self.assertFalse(parsed.add_approvals)
+ self.assertItemsEqual(parsed.phase_names, ['', '', '', '', '', ''])
+ self.assertEqual(parsed.approvals_to_phase_idx, {})
+ self.assertEqual(parsed.required_approval_ids, [])
+
+ def testParseTemplateRequest_Normal(self):
+ self.config.field_defs.extend([self.fd_1, self.fd_2])
+ self.config.approval_defs.extend([self.ad_3, self.ad_4])
+ post_data = fake.PostData(
+ name=['sometemplate'],
+ members_only=['on'],
+ summary=['TLDR'],
+ summary_must_be_edited=['on'],
+ content=['HEY WHY'],
+ status=['Accepted'],
+ owner=['someone@world.com'],
+ label=['label-One', 'label-Two'],
+ custom_1=['NO'],
+ custom_2=['MOOD'],
+ components=['hey, hey2,he3'],
+ component_required=['on'],
+ owner_defaults_to_memeber=['no'],
+ admin_names=['jojwang@test.com, annajo@test.com'],
+ add_approvals=['on'],
+ phase_0=['Canary'],
+ phase_1=['Stable-Exp'],
+ phase_2=['Stable'],
+ phase_3=[''],
+ phase_4=[''],
+ phase_5=['Oops'],
+ approval_3=['phase_2'],
+ approval_4=['no_phase'],
+ approval_3_required=['on'],
+ approval_4_required=['on'],
+ # ignore required cb for omitted approvals
+ approval_5_required=['on']
+ )
+
+ parsed = template_helpers.ParseTemplateRequest(post_data, self.config)
+ self.assertEqual(parsed.name, 'sometemplate')
+ self.assertTrue(parsed.members_only)
+ self.assertEqual(parsed.summary, 'TLDR')
+ self.assertTrue(parsed.summary_must_be_edited)
+ self.assertEqual(parsed.content, 'HEY WHY')
+ self.assertEqual(parsed.status, 'Accepted')
+ self.assertEqual(parsed.owner_str, 'someone@world.com')
+ self.assertEqual(parsed.labels, ['label-One', 'label-Two'])
+ self.assertEqual(parsed.field_val_strs, {1: ['NO'], 2: ['MOOD']})
+ self.assertEqual(parsed.component_paths, ['hey', 'hey2', 'he3'])
+ self.assertTrue(parsed.component_required)
+ self.assertFalse(parsed.owner_defaults_to_member)
+ self.assertTrue(parsed.add_approvals)
+ self.assertEqual(parsed.admin_str, 'jojwang@test.com, annajo@test.com')
+ self.assertItemsEqual(parsed.phase_names,
+ ['Canary', 'Stable-Exp', 'Stable', '', '', 'Oops'])
+ self.assertEqual(parsed.approvals_to_phase_idx, {3: 2, 4: None})
+ self.assertItemsEqual(parsed.required_approval_ids, [3, 4])
+
+ def testGetTemplateInfoFromParsed_Normal(self):
+ self.config.field_defs.extend([self.fd_1, self.fd_2])
+ self.config.component_defs.append(self.cd_1)
+ parsed = template_helpers.ParsedTemplate(
+ 'template', True, 'summary', True, 'content', 'Available',
+ '1@ex.com', ['label1', 'label1'], {1: ['NO'], 2: ['MOOD']},
+ ['BackEnd'], True, True, '2@ex.com', False, [], {}, [])
+ (admin_ids, owner_id, component_ids,
+ field_values, phases,
+ approval_values) = template_helpers.GetTemplateInfoFromParsed(
+ self.mr, self.services, parsed, self.config)
+ self.assertEqual(admin_ids, [222])
+ self.assertEqual(owner_id, 111)
+ self.assertEqual(component_ids, [1])
+ self.assertEqual(field_values[0].str_value, 'NO')
+ self.assertEqual(field_values[1].str_value, 'MOOD')
+ self.assertEqual(phases, [])
+ self.assertEqual(approval_values, [])
+
+ def testGetTemplateInfoFromParsed_Errors(self):
+ self.config.field_defs.extend([self.fd_1, self.fd_2])
+ parsed = template_helpers.ParsedTemplate(
+ 'template', True, 'summary', True, 'content', 'Available',
+ '4@ex.com', ['label1', 'label1'], {1: ['NO'], 2: ['MOOD']},
+ ['BackEnd'], True, True, '2@ex.com', False, [], {}, [])
+ (admin_ids, _owner_id, _component_ids,
+ field_values, phases,
+ approval_values) = template_helpers.GetTemplateInfoFromParsed(
+ self.mr, self.services, parsed, self.config)
+ self.assertEqual(admin_ids, [222])
+ self.assertEqual(field_values[0].str_value, 'NO')
+ self.assertEqual(field_values[1].str_value, 'MOOD')
+ self.assertEqual(self.mr.errors.owner, 'Owner not found.')
+ self.assertEqual(self.mr.errors.components, 'Unknown component BackEnd')
+ self.assertEqual(phases, [])
+ self.assertEqual(approval_values, [])
+
+ def testGetPhasesAndApprovalsFromParsed_Normal(self):
+ self.config.field_defs.extend([self.fd_1, self.fd_2])
+ self.config.approval_defs.extend([self.ad_3, self.ad_4, self.ad_5])
+
+ phase_names = ['Canary', '', 'Stable-Exp', '', '', '']
+ approvals_to_phase_idx = {3: 0, 4: None, 5: 2}
+ required_approval_ids = [3, 5]
+
+ phases, approval_values = template_helpers._GetPhasesAndApprovalsFromParsed(
+ self.mr, phase_names, approvals_to_phase_idx, required_approval_ids)
+ self.assertEqual(len(phases), 2)
+ self.assertEqual(len(approval_values), 3)
+
+ canary = tracker_bizobj.FindPhase('canary', phases)
+ self.assertEqual(canary.rank, 0)
+ av_3 = tracker_bizobj.FindApprovalValueByID(3, approval_values)
+ self.assertEqual(av_3.status, tracker_pb2.ApprovalStatus.NEEDS_REVIEW)
+ self.assertEqual(av_3.phase_id, canary.phase_id)
+
+ av_4 = tracker_bizobj.FindApprovalValueByID(4, approval_values)
+ self.assertEqual(av_4.status, tracker_pb2.ApprovalStatus.NOT_SET)
+ self.assertIsNone(av_4.phase_id)
+
+ stable_exp = tracker_bizobj.FindPhase('stable-exp', phases)
+ self.assertEqual(stable_exp.rank, 2)
+ av_5 = tracker_bizobj.FindApprovalValueByID(5, approval_values)
+ self.assertEqual(av_5.status, tracker_pb2.ApprovalStatus.NEEDS_REVIEW)
+ self.assertEqual(av_5.phase_id, stable_exp.phase_id)
+
+ self.assertIsNone(self.mr.errors.phase_approvals)
+
+ def testGetPhasesAndApprovalsFromParsed_Errors(self):
+ self.config.field_defs.extend([self.fd_1, self.fd_2])
+ self.config.approval_defs.extend([self.ad_3, self.ad_4, self.ad_5])
+ required_approval_ids = []
+
+ phase_names = ['Canary', 'Extra', 'Stable-Exp', '', '', '']
+ approvals_to_phase_idx = {3: 0, 4: None, 5: 2}
+
+ template_helpers._GetPhasesAndApprovalsFromParsed(
+ self.mr, phase_names, approvals_to_phase_idx, required_approval_ids)
+ self.assertEqual(self.mr.errors.phase_approvals,
+ 'Defined gates must have assigned approvals.')
+
+ def testGetPhasesAndApprovalsFromParsed_DupsErrors(self):
+ self.config.field_defs.extend([self.fd_1, self.fd_2])
+ self.config.approval_defs.extend([self.ad_3, self.ad_4, self.ad_5])
+ required_approval_ids = []
+
+ phase_names = ['Canary', 'canary', 'Stable-Exp', '', '', '']
+ approvals_to_phase_idx = {3: 0, 4: None, 5: 2}
+
+ template_helpers._GetPhasesAndApprovalsFromParsed(
+ self.mr, phase_names, approvals_to_phase_idx, required_approval_ids)
+ self.assertEqual(self.mr.errors.phase_approvals,
+ 'Duplicate gate names.')
+
+ def testGetPhasesAndApprovalsFromParsed_InvalidPhaseName(self):
+ self.config.field_defs.extend([self.fd_1, self.fd_2])
+ self.config.approval_defs.extend([self.ad_3, self.ad_4, self.ad_5])
+ required_approval_ids = []
+
+ phase_names = ['Canary', 'A B', 'Stable-Exp', '', '', '']
+ approvals_to_phase_idx = {3: 0, 4: None, 5: 2}
+
+ template_helpers._GetPhasesAndApprovalsFromParsed(
+ self.mr, phase_names, approvals_to_phase_idx, required_approval_ids)
+ self.assertEqual(self.mr.errors.phase_approvals,
+ 'Invalid gate name(s).')
+
+ def testGatherApprovalsPageData(self):
+ self.fd_3.is_deleted = True
+ self.config.field_defs = [self.fd_3, self.fd_4, self.fd_5]
+ approval_values = [
+ tracker_pb2.ApprovalValue(approval_id=3, phase_id=8),
+ tracker_pb2.ApprovalValue(
+ approval_id=4, phase_id=9,
+ status=tracker_pb2.ApprovalStatus.NEEDS_REVIEW),
+ tracker_pb2.ApprovalValue(approval_id=5)
+ ]
+ tmpl_phases = [
+ tracker_pb2.Phase(phase_id=8, rank=1, name='deletednoshow'),
+ tracker_pb2.Phase(phase_id=9, rank=2, name='notdeleted')
+ ]
+
+ (prechecked_approvals, required_approval_ids,
+ phases) = template_helpers.GatherApprovalsPageData(
+ approval_values, tmpl_phases, self.config)
+ self.assertItemsEqual(prechecked_approvals,
+ ['4_phase_0', '5'])
+ self.assertEqual(required_approval_ids, [4])
+ self.assertEqual(phases[0], tmpl_phases[1])
+ self.assertIsNone(phases[1].name)
+ self.assertEqual(len(phases), 6)
+
+ def testGetCheckedApprovalsFromParsed(self):
+ approvals_to_phase_idx = {23: 0, 25: 1, 26: None}
+ checked = template_helpers.GetCheckedApprovalsFromParsed(
+ approvals_to_phase_idx)
+ self.assertItemsEqual(checked,
+ ['23_phase_0', '25_phase_1', '26'])
+
+ def testGetIssueFromTemplate(self):
+ """Can fill and return the templated issue"""
+ expected_fvs = [
+ tracker_pb2.FieldValue(field_id=123, str_value='fv_1_value'),
+ tracker_pb2.FieldValue(field_id=124, str_value='fv_2_value'),
+ ]
+ expected_phases = [
+ tracker_pb2.Phase(phase_id=123, name='phase_1_name', rank=1)
+ ]
+ expected_avs = [
+ tracker_pb2.ApprovalValue(
+ approval_id=1,
+ setter_id=111,
+ set_on=1232352,
+ approver_ids=[111],
+ phase_id=123),
+ ]
+ input_template = tracker_pb2.TemplateDef(
+ summary='expected_summary',
+ owner_id=111,
+ status='expected_status',
+ labels=['expected-label_1, expected-label_2'],
+ field_values=expected_fvs,
+ component_ids=[987],
+ phases=expected_phases,
+ approval_values=expected_avs)
+ reporter_id = 321
+ project_id = 1
+
+ actual = template_helpers.GetIssueFromTemplate(
+ input_template, project_id, reporter_id)
+ expected = tracker_pb2.Issue(
+ project_id=project_id,
+ summary='expected_summary',
+ status='expected_status',
+ owner_id=111,
+ labels=['expected-label_1, expected-label_2'],
+ component_ids=[987],
+ reporter_id=reporter_id,
+ field_values=expected_fvs,
+ phases=expected_phases,
+ approval_values=expected_avs)
+ self.assertEqual(actual, expected)
+
+ def testGetIssueFromTemplate_NoOwner(self):
+ """Uses reporter as owner when owner_defaults_to_member"""
+ input_template = tracker_pb2.TemplateDef(owner_defaults_to_member=False)
+
+ actual = template_helpers.GetIssueFromTemplate(input_template, 1, 1)
+ self.assertEqual(actual.owner_id, None)
+
+ def testGetIssueFromTemplate_DefaultsOwnerToReporter(self):
+ """Uses reporter as owner when owner_defaults_to_member"""
+ input_template = tracker_pb2.TemplateDef(owner_defaults_to_member=True)
+ reporter_id = 321
+
+ actual = template_helpers.GetIssueFromTemplate(
+ input_template, 1, reporter_id)
+ self.assertEqual(actual.owner_id, reporter_id)
+
+ def testGetIssueFromTemplate_SpecifiedOwnerOverridesReporter(self):
+ """Specified owner overrides owner_defaults_to_member"""
+ expected_owner_id = 111
+ input_template = tracker_pb2.TemplateDef(
+ owner_id=expected_owner_id, owner_defaults_to_member=True)
+ reporter_id = 321
+
+ actual = template_helpers.GetIssueFromTemplate(
+ input_template, 1, reporter_id)
+ self.assertEqual(actual.owner_id, expected_owner_id)
diff --git a/tracker/test/templatecreate_test.py b/tracker/test/templatecreate_test.py
new file mode 100644
index 0000000..60db78b
--- /dev/null
+++ b/tracker/test/templatecreate_test.py
@@ -0,0 +1,374 @@
+# 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
+
+"""Unit test for Template creation servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mox
+import unittest
+import settings
+
+from mock import Mock
+
+import ezt
+
+from framework import permissions
+from services import service_manager
+from services import template_svc
+from testing import fake
+from testing import testing_helpers
+from tracker import templatecreate
+from tracker import tracker_bizobj
+from tracker import tracker_views
+from proto import tracker_pb2
+
+
+class TemplateCreateTest(unittest.TestCase):
+ """Tests for the TemplateCreate servlet."""
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.services = service_manager.Services(
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ template=Mock(spec=template_svc.TemplateService),
+ user=fake.UserService())
+ self.servlet = templatecreate.TemplateCreate('req', 'res',
+ services=self.services)
+ self.project = self.services.project.TestAddProject('proj')
+
+ self.fd_1 = tracker_bizobj.MakeFieldDef(
+ 1, self.project.project_id, 'StringFieldName',
+ tracker_pb2.FieldTypes.STR_TYPE, None, '', False,
+ False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'some approval thing', False, approval_id=2)
+
+ self.fd_2 = tracker_bizobj.MakeFieldDef(
+ 2, self.project.project_id, 'UXApproval',
+ tracker_pb2.FieldTypes.APPROVAL_TYPE, None, '', False, False, False,
+ None, None, '', False, '', '', tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action', 'Approval for UX review', False)
+ self.fd_3 = tracker_bizobj.MakeFieldDef(
+ 3, self.project.project_id, 'TestApproval',
+ tracker_pb2.FieldTypes.APPROVAL_TYPE, None, '', False, False, False,
+ None, None, '', False, '', '', tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action', 'Approval for Test review', False)
+ self.fd_4 = tracker_bizobj.MakeFieldDef(
+ 4, self.project.project_id, 'Target',
+ tracker_pb2.FieldTypes.INT_TYPE, None, '', False, False, False, None,
+ None, '', False, '', '', tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'milestone target', False, is_phase_field=True)
+ self.fd_5 = tracker_bizobj.MakeFieldDef(
+ 5,
+ self.project.project_id,
+ 'RestrictedField',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'RestrictedField',
+ False,
+ is_restricted_field=True)
+ self.fd_6 = tracker_bizobj.MakeFieldDef(
+ 6,
+ self.project.project_id,
+ 'RestrictedEnumField',
+ tracker_pb2.FieldTypes.ENUM_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'RestrictedEnumField',
+ False,
+ is_restricted_field=True)
+ ad_2 = tracker_pb2.ApprovalDef(approval_id=2)
+ ad_3 = tracker_pb2.ApprovalDef(approval_id=3)
+
+ self.config = self.services.config.GetProjectConfig(
+ 'fake cnxn', self.project.project_id)
+ self.config.approval_defs.extend([ad_2, ad_3])
+ self.config.field_defs.extend(
+ [self.fd_1, self.fd_2, self.fd_3, self.fd_4, self.fd_5, self.fd_6])
+
+ first_tmpl = tracker_bizobj.MakeIssueTemplate(
+ 'sometemplate', 'summary', None, None, 'content', [], [], [],
+ [])
+ self.services.config.StoreConfig(None, self.config)
+
+ templates = testing_helpers.DefaultTemplates()
+ templates.append(first_tmpl)
+ self.services.template.GetProjectTemplates = Mock(
+ return_value=templates)
+
+ self.mr = testing_helpers.MakeMonorailRequest(
+ project=self.project, perms=permissions.OWNER_ACTIVE_PERMISSIONSET)
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testAssertBasePermission(self):
+ # Anon users can never do it
+ self.mr.perms = permissions.READ_ONLY_PERMISSIONSET
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, self.mr)
+
+ # Project owner can do it.
+ self.mr.perms = permissions.OWNER_ACTIVE_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+
+ # Project member cannot do it
+ self.mr.perms = permissions.COMMITTER_ACTIVE_PERMISSIONSET
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, self.mr)
+ self.mr.perms = permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, self.mr)
+
+ def testGatherPageData(self):
+ precomp_view_info = tracker_views._PrecomputeInfoForValueViews(
+ [], [], [], self.config, [])
+ fv = tracker_views._MakeFieldValueView(
+ self.fd_1, self.config, precomp_view_info, {})
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual(self.servlet.PROCESS_TAB_TEMPLATES,
+ page_data['admin_tab_mode'])
+ self.assertTrue(page_data['allow_edit'])
+ self.assertEqual(page_data['uneditable_fields'], ezt.boolean(False))
+ self.assertTrue(page_data['new_template_form'])
+ self.assertFalse(page_data['initial_members_only'])
+ self.assertEqual(page_data['template_name'], '')
+ self.assertEqual(page_data['initial_summary'], '')
+ self.assertFalse(page_data['initial_must_edit_summary'])
+ self.assertEqual(page_data['initial_content'], '')
+ self.assertEqual(page_data['initial_status'], '')
+ self.assertEqual(page_data['initial_owner'], '')
+ self.assertFalse(page_data['initial_owner_defaults_to_member'])
+ self.assertEqual(page_data['initial_components'], '')
+ self.assertFalse(page_data['initial_component_required'])
+ self.assertEqual(page_data['fields'][2].field_name, fv.field_name)
+ self.assertEqual(page_data['initial_admins'], '')
+ self.assertEqual(page_data['approval_subfields_present'], ezt.boolean(True))
+ self.assertEqual(page_data['phase_fields_present'], ezt.boolean(False))
+
+ def testProcessFormData_Reject(self):
+ self.services.user.TestAddUser('user@example.com', 222)
+ self.mr.auth.effective_ids = {222}
+ post_data = fake.PostData(
+ name=['sometemplate'],
+ members_only=['on'],
+ summary=['TLDR'],
+ summary_must_be_edited=['on'],
+ content=['HEY WHY'],
+ status=['Accepted'],
+ owner=['someone@world.com'],
+ label=['label-One', 'label-Two'],
+ custom_1=['NO'],
+ custom_2=['MOOD'],
+ components=['hey, hey2,he3'],
+ component_required=['on'],
+ owner_defaults_to_member=['no'],
+ add_approvals = ['on'],
+ phase_0=['Canary'],
+ phase_1=['Stable-Exp'],
+ phase_2=['Stable'],
+ phase_3=[''],
+ phase_4=[''],
+ phase_5=[''],
+ approval_2=['phase_1'],
+ approval_3=['phase_2']
+ )
+
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ self.mr,
+ initial_members_only=ezt.boolean(True),
+ template_name='sometemplate',
+ initial_content='TLDR',
+ initial_must_edit_summary=ezt.boolean(True),
+ initial_description='HEY WHY',
+ initial_status='Accepted',
+ initial_owner='someone@world.com',
+ initial_owner_defaults_to_member=ezt.boolean(False),
+ initial_components='hey, hey2, he3',
+ initial_component_required=ezt.boolean(True),
+ initial_admins='',
+ labels=['label-One', 'label-Two'],
+ fields=mox.IgnoreArg(),
+ initial_add_approvals=ezt.boolean(True),
+ initial_phases=[tracker_pb2.Phase(name=name) for
+ name in ['Canary', 'Stable-Exp', 'Stable', '', '', '']],
+ approvals=mox.IgnoreArg(),
+ prechecked_approvals=['2_phase_1', '3_phase_2'],
+ required_approval_ids=[]
+ )
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual('Owner not found.', self.mr.errors.owner)
+ self.assertEqual('Unknown component he3', self.mr.errors.components)
+ self.assertEqual(
+ 'Template with name sometemplate already exists', self.mr.errors.name)
+ self.assertEqual('Defined gates must have assigned approvals.',
+ self.mr.errors.phase_approvals)
+ self.assertIsNone(url)
+
+ def testProcessFormData_RejectRestrictedFields(self):
+ self.services.template.GetTemplateByName = Mock(return_value=None)
+ self.mr.perms = permissions.PermissionSet([])
+ post_data_add_fv = fake.PostData(
+ name=['secondtemplate'],
+ members_only=['on'],
+ summary=['TLDR'],
+ summary_must_be_edited=['on'],
+ content=['HEY WHY'],
+ status=['Accepted'],
+ label=['label-One', 'label-Two'],
+ custom_1=['Hey'],
+ custom_5=['7'],
+ component_required=['on'],
+ owner_defaults_to_member=['no'],
+ add_approvals=['no'],
+ phase_0=[''],
+ phase_1=[''],
+ phase_2=[''],
+ phase_3=[''],
+ phase_4=[''],
+ phase_5=['OOPs'],
+ approval_2=['phase_0'],
+ approval_3=['phase_2'])
+ post_data_label_edits_enum = fake.PostData(
+ name=['secondtemplate'],
+ members_only=['on'],
+ summary=['TLDR'],
+ summary_must_be_edited=['on'],
+ content=['HEY WHY'],
+ status=['Accepted'],
+ label=['label-One', 'label-Two', 'RestrictedEnumField-7'],
+ component_required=['on'],
+ owner_defaults_to_member=['no'],
+ add_approvals=['no'],
+ phase_0=[''],
+ phase_1=[''],
+ phase_2=[''],
+ phase_3=[''],
+ phase_4=[''],
+ phase_5=['OOPs'],
+ approval_2=['phase_0'],
+ approval_3=['phase_2'])
+
+ self.assertRaises(
+ AssertionError, self.servlet.ProcessFormData, self.mr, post_data_add_fv)
+ self.assertRaises(
+ AssertionError, self.servlet.ProcessFormData, self.mr,
+ post_data_label_edits_enum)
+
+ def testProcessFormData_Accept(self):
+ self.services.user.TestAddUser('user@example.com', 222)
+ self.mr.auth.effective_ids = {222}
+ self.services.template.GetTemplateByName = Mock(return_value=None)
+ post_data = fake.PostData(
+ name=['secondtemplate'],
+ members_only=['on'],
+ summary=['TLDR'],
+ summary_must_be_edited=['on'],
+ content=['HEY WHY'],
+ status=['Accepted'],
+ label=['label-One', 'label-Two', 'RestrictedEnumField-7'],
+ custom_1=['NO'],
+ custom_5=['37'],
+ component_required=['on'],
+ owner_defaults_to_member=['no'],
+ add_approvals=['no'],
+ phase_0=[''],
+ phase_1=[''],
+ phase_2=[''],
+ phase_3=[''],
+ phase_4=[''],
+ phase_5=['OOPs'],
+ approval_2=['phase_0'],
+ approval_3=['phase_2'])
+
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+
+ self.assertTrue('/adminTemplates?saved=1&ts' in url)
+
+ self.assertEqual(0,
+ self.services.template.UpdateIssueTemplateDef.call_count)
+
+ # errors in phases should not matter if add_approvals is not 'on'
+ self.assertIsNone(self.mr.errors.phase_approvals)
+
+ def testProcessFormData_AcceptPhases(self):
+ self.services.user.TestAddUser('user@example.com', 222)
+ self.mr.auth.effective_ids = {222}
+ self.services.template.GetTemplateByName = Mock(return_value=None)
+ post_data = fake.PostData(
+ name=['secondtemplate'],
+ members_only=['on'],
+ summary=['TLDR'],
+ summary_must_be_edited=['on'],
+ content=['HEY WHY'],
+ status=['Accepted'],
+ label=['label-One', 'label-Two'],
+ custom_1=['NO'],
+ component_required=['on'],
+ owner_defaults_to_member=['no'],
+ add_approvals = ['on'],
+ phase_0=['Canary'],
+ phase_1=['Stable'],
+ phase_2=[''],
+ phase_3=[''],
+ phase_4=[''],
+ phase_5=[''],
+ approval_2=['phase_0'],
+ approval_3=['phase_1'],
+ approval_3_required=['on']
+ )
+
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertTrue('/adminTemplates?saved=1&ts' in url)
+
+ fv = tracker_pb2.FieldValue(field_id=1, str_value='NO', derived=False)
+ phases = [
+ tracker_pb2.Phase(name='Canary', rank=0, phase_id=0),
+ tracker_pb2.Phase(name='Stable', rank=1, phase_id=1)
+ ]
+ approval_values = [
+ tracker_pb2.ApprovalValue(approval_id=2, phase_id=0),
+ tracker_pb2.ApprovalValue(
+ approval_id=3, status=tracker_pb2.ApprovalStatus(
+ tracker_pb2.ApprovalStatus.NEEDS_REVIEW), phase_id=1)
+ ]
+ self.services.template.CreateIssueTemplateDef.assert_called_once_with(
+ self.mr.cnxn, 47925, 'secondtemplate', 'HEY WHY', 'TLDR', True,
+ 'Accepted', True, False, True, 0, ['label-One', 'label-Two'], [], [],
+ [fv], phases=phases, approval_values=approval_values)
diff --git a/tracker/test/templatedetail_test.py b/tracker/test/templatedetail_test.py
new file mode 100644
index 0000000..607996a
--- /dev/null
+++ b/tracker/test/templatedetail_test.py
@@ -0,0 +1,521 @@
+# 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
+
+"""Unit tests for Template editing/viewing servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mox
+import logging
+import unittest
+import settings
+
+from mock import Mock
+
+import ezt
+
+from framework import permissions
+from services import service_manager
+from services import template_svc
+from testing import fake
+from testing import testing_helpers
+from tracker import templatedetail
+from tracker import tracker_bizobj
+from proto import tracker_pb2
+
+
+class TemplateDetailTest(unittest.TestCase):
+ """Tests for the TemplateDetail servlet."""
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ mock_template_service = Mock(spec=template_svc.TemplateService)
+ self.services = service_manager.Services(project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ template=mock_template_service,
+ usergroup=fake.UserGroupService(),
+ user=fake.UserService())
+ self.servlet = templatedetail.TemplateDetail('req', 'res',
+ services=self.services)
+
+ self.services.user.TestAddUser('gatsby@example.com', 111)
+ self.services.user.TestAddUser('sport@example.com', 222)
+ self.services.user.TestAddUser('gatsby@example.com', 111)
+ self.services.user.TestAddUser('daisy@example.com', 333)
+
+ self.project = self.services.project.TestAddProject('proj')
+ self.services.project.TestAddProjectMembers(
+ [333], self.project, 'CONTRIBUTOR_ROLE')
+
+ self.template = self.test_template = tracker_bizobj.MakeIssueTemplate(
+ 'TestTemplate', 'sum', 'New', 111, 'content', ['label1', 'label2'],
+ [], [222], [], summary_must_be_edited=True,
+ owner_defaults_to_member=True, component_required=False,
+ members_only=False)
+ self.template.template_id = 12345
+ self.services.template.GetTemplateByName = Mock(
+ return_value=self.template)
+
+ self.mr = testing_helpers.MakeMonorailRequest(project=self.project)
+ self.mr.template_name = 'TestTemplate'
+
+ self.mox = mox.Mox()
+
+ self.fd_1 = tracker_bizobj.MakeFieldDef(
+ 1, 789, 'UXReview', tracker_pb2.FieldTypes.STR_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'Approval for UX review', False, approval_id=2)
+ self.fd_2 = tracker_bizobj.MakeFieldDef(
+ 2, 789, 'UXReview', tracker_pb2.FieldTypes.STR_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'Approval for UX review', False)
+ self.fd_3 = tracker_bizobj.MakeFieldDef(
+ 3, 789, 'TestApproval', tracker_pb2.FieldTypes.APPROVAL_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'Approval for Test',
+ False)
+ self.fd_4 = tracker_bizobj.MakeFieldDef(
+ 4, 789, 'SecurityApproval', tracker_pb2.FieldTypes.APPROVAL_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'Approval for Security',
+ False)
+ self.fd_5 = tracker_bizobj.MakeFieldDef(
+ 5, 789, 'GateTarget', tracker_pb2.FieldTypes.INT_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'milestone target', False, is_phase_field=True)
+ self.fd_6 = tracker_bizobj.MakeFieldDef(
+ 6, 789, 'Choices', tracker_pb2.FieldTypes.ENUM_TYPE, None,
+ '', False, False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'milestone target', False, is_phase_field=True)
+ self.fd_7 = tracker_bizobj.MakeFieldDef(
+ 7,
+ 789,
+ 'RestrictedField',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'RestrictedField',
+ False,
+ is_restricted_field=True)
+ self.fd_8 = tracker_bizobj.MakeFieldDef(
+ 8,
+ 789,
+ 'RestrictedEnumField',
+ tracker_pb2.FieldTypes.ENUM_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'RestrictedEnumField',
+ False,
+ is_restricted_field=True)
+ self.fd_9 = tracker_bizobj.MakeFieldDef(
+ 9,
+ 789,
+ 'RestrictedField_2',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'RestrictedField_2',
+ False,
+ is_restricted_field=True)
+ self.fd_10 = tracker_bizobj.MakeFieldDef(
+ 10,
+ 789,
+ 'RestrictedEnumField_2',
+ tracker_pb2.FieldTypes.ENUM_TYPE,
+ None,
+ '',
+ False,
+ False,
+ False,
+ None,
+ None,
+ '',
+ False,
+ '',
+ '',
+ tracker_pb2.NotifyTriggers.NEVER,
+ 'no_action',
+ 'RestrictedEnumField_2',
+ False,
+ is_restricted_field=True)
+
+ self.ad_3 = tracker_pb2.ApprovalDef(approval_id=3)
+ self.ad_4 = tracker_pb2.ApprovalDef(approval_id=4)
+
+ self.cd_1 = tracker_bizobj.MakeComponentDef(
+ 1, 789, 'BackEnd', 'doc', False, [111], [], 100000, 222)
+ self.template.component_ids.append(1)
+
+ self.canary_phase = tracker_pb2.Phase(
+ name='Canary', phase_id=1, rank=1)
+ self.av_3 = tracker_pb2.ApprovalValue(approval_id=3, phase_id=1)
+ self.stable_phase = tracker_pb2.Phase(
+ name='Stable', phase_id=2, rank=3)
+ self.av_4 = tracker_pb2.ApprovalValue(approval_id=4, phase_id=2)
+ self.template.phases.extend([self.stable_phase, self.canary_phase])
+ self.template.approval_values.extend([self.av_3, self.av_4])
+
+ self.config = self.services.config.GetProjectConfig(
+ 'fake cnxn', self.project.project_id)
+ self.templates = testing_helpers.DefaultTemplates()
+ self.template.labels.extend(
+ ['GateTarget-Should-Not', 'GateTarget-Be-Masked',
+ 'Choices-Wrapped', 'Choices-Burritod'])
+ self.templates.append(self.template)
+ self.services.template.GetProjectTemplates = Mock(
+ return_value=self.templates)
+ self.services.template.FindTemplateByName = Mock(return_value=self.template)
+ self.config.component_defs.append(self.cd_1)
+ self.config.field_defs.extend(
+ [
+ self.fd_1, self.fd_2, self.fd_3, self.fd_4, self.fd_5, self.fd_6,
+ self.fd_7, self.fd_8, self.fd_9, self.fd_10
+ ])
+ self.config.approval_defs.extend([self.ad_3, self.ad_4])
+ self.services.config.StoreConfig(None, self.config)
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testAssertBasePermission_Anyone(self):
+ self.mr.auth.effective_ids = {222}
+ self.servlet.AssertBasePermission(self.mr)
+
+ self.mr.auth.effective_ids = {333}
+ self.servlet.AssertBasePermission(self.mr)
+
+ self.mr.perms = permissions.OWNER_ACTIVE_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+
+ self.mr.perms = permissions.READ_ONLY_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+
+ def testAssertBasePermision_MembersOnly(self):
+ self.template.members_only = True
+ self.mr.auth.effective_ids = {222}
+ self.servlet.AssertBasePermission(self.mr)
+
+ self.mr.auth.effective_ids = {333}
+ self.servlet.AssertBasePermission(self.mr)
+
+ self.mr.auth.effective_ids = {444}
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, self.mr)
+
+ self.mr.perms = permissions.READ_ONLY_PERMISSIONSET
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, self.mr)
+
+ def testGatherPageData(self):
+ self.mr.perms = permissions.PermissionSet([])
+ self.mr.auth.effective_ids = {222} # template admin
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual(self.servlet.PROCESS_TAB_TEMPLATES,
+ page_data['admin_tab_mode'])
+ self.assertTrue(page_data['allow_edit'])
+ self.assertEqual(page_data['uneditable_fields'], ezt.boolean(True))
+ self.assertFalse(page_data['new_template_form'])
+ self.assertFalse(page_data['initial_members_only'])
+ self.assertEqual(page_data['template_name'], 'TestTemplate')
+ self.assertEqual(page_data['initial_summary'], 'sum')
+ self.assertTrue(page_data['initial_must_edit_summary'])
+ self.assertEqual(page_data['initial_content'], 'content')
+ self.assertEqual(page_data['initial_status'], 'New')
+ self.assertEqual(page_data['initial_owner'], 'gatsby@example.com')
+ self.assertTrue(page_data['initial_owner_defaults_to_member'])
+ self.assertEqual(page_data['initial_components'], 'BackEnd')
+ self.assertFalse(page_data['initial_component_required'])
+ self.assertItemsEqual(
+ page_data['labels'],
+ ['label1', 'label2', 'GateTarget-Should-Not', 'GateTarget-Be-Masked'])
+ self.assertEqual(page_data['initial_admins'], 'sport@example.com')
+ self.assertTrue(page_data['initial_add_approvals'])
+ self.assertEqual(len(page_data['initial_phases']), 6)
+ phases = [phase for phase in page_data['initial_phases'] if phase.name]
+ self.assertEqual(len(phases), 2)
+ self.assertEqual(len(page_data['approvals']), 2)
+ self.assertItemsEqual(page_data['prechecked_approvals'],
+ ['3_phase_0', '4_phase_1'])
+ self.assertTrue(page_data['fields'][3].is_editable) #nonRestrictedField
+ self.assertIsNone(page_data['fields'][4].is_editable) #restrictedField
+
+ def testProcessFormData_Reject(self):
+ self.mr.auth.effective_ids = {222}
+ post_data = fake.PostData(
+ name=['TestTemplate'],
+ members_only=['on'],
+ summary=['TLDR'],
+ summary_must_be_edited=['on'],
+ content=['HEY WHY'],
+ status=['Accepted'],
+ owner=['someone@world.com'],
+ label=['label-One', 'label-Two'],
+ custom_1=['NO'],
+ custom_2=['MOOD'],
+ components=['hey, hey2,he3'],
+ component_required=['on'],
+ owner_defaults_to_member=['no'],
+ add_approvals = ['on'],
+ phase_0=['Canary'],
+ phase_1=['Stable-Exp'],
+ phase_2=['Stable'],
+ phase_3=[''],
+ phase_4=[''],
+ phase_5=[''],
+ approval_3=['phase_0'],
+ approval_4=['phase_2']
+ )
+
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ self.mr,
+ initial_members_only=ezt.boolean(True),
+ template_name='TestTemplate',
+ initial_summary='TLDR',
+ initial_must_edit_summary=ezt.boolean(True),
+ initial_content='HEY WHY',
+ initial_status='Accepted',
+ initial_owner='someone@world.com',
+ initial_owner_defaults_to_member=ezt.boolean(False),
+ initial_components='hey, hey2, he3',
+ initial_component_required=ezt.boolean(True),
+ initial_admins='',
+ labels=['label-One', 'label-Two'],
+ fields=mox.IgnoreArg(),
+ initial_add_approvals=ezt.boolean(True),
+ initial_phases=[tracker_pb2.Phase(name=name) for
+ name in ['Canary', 'Stable-Exp', 'Stable', '', '', '']],
+ approvals=mox.IgnoreArg(),
+ prechecked_approvals=['3_phase_0', '4_phase_2'],
+ required_approval_ids=[]
+ )
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual('Owner not found.', self.mr.errors.owner)
+ self.assertEqual('Unknown component he3', self.mr.errors.components)
+ self.assertIsNone(url)
+ self.assertEqual('Defined gates must have assigned approvals.',
+ self.mr.errors.phase_approvals)
+
+ def testProcessFormData_RejectRestrictedFields(self):
+ """Template admins cannot set restricted fields by default."""
+ self.mr.perms = permissions.PermissionSet([])
+ self.mr.auth.effective_ids = {222} # template admin
+ post_data_add_fv = fake.PostData(
+ name=['TestTemplate'],
+ members_only=['on'],
+ summary=['TLDR'],
+ summary_must_be_edited=[''],
+ content=['HEY WHY'],
+ status=['Accepted'],
+ owner=['daisy@example.com'],
+ label=['label-One', 'label-Two'],
+ custom_1=['NO'],
+ custom_2=['MOOD'],
+ custom_7=['37'],
+ components=['BackEnd'],
+ component_required=['on'],
+ owner_defaults_to_member=['on'],
+ add_approvals=['no'],
+ phase_0=[''],
+ phase_1=[''],
+ phase_2=[''],
+ phase_3=[''],
+ phase_4=[''],
+ phase_5=['OOPs'],
+ approval_3=['phase_0'],
+ approval_4=['phase_2'])
+ post_data_label_edits_enum = fake.PostData(
+ name=['TestTemplate'],
+ members_only=['on'],
+ summary=['TLDR'],
+ summary_must_be_edited=[''],
+ content=['HEY WHY'],
+ status=['Accepted'],
+ owner=['daisy@example.com'],
+ label=['label-One', 'label-Two', 'RestrictedEnumField-7'],
+ components=['BackEnd'],
+ component_required=['on'],
+ owner_defaults_to_member=['on'],
+ add_approvals=['no'],
+ phase_0=[''],
+ phase_1=[''],
+ phase_2=[''],
+ phase_3=[''],
+ phase_4=[''],
+ phase_5=['OOPs'],
+ approval_3=['phase_0'],
+ approval_4=['phase_2'])
+
+ self.assertRaises(
+ AssertionError, self.servlet.ProcessFormData, self.mr, post_data_add_fv)
+ self.assertRaises(
+ AssertionError, self.servlet.ProcessFormData, self.mr,
+ post_data_label_edits_enum)
+
+ def testProcessFormData_Accept(self):
+ self.fd_7.editor_ids = [222]
+ self.fd_8.editor_ids = [222]
+ self.mr.perms = permissions.PermissionSet([])
+ self.mr.auth.effective_ids = {222} # template admin
+ temp_restricted_fv = tracker_bizobj.MakeFieldValue(
+ 9, 3737, None, None, None, None, False)
+ self.template.field_values.append(temp_restricted_fv)
+ self.template.labels.append('RestrictedEnumField_2-b')
+ post_data = fake.PostData(
+ name=['TestTemplate'],
+ members_only=['on'],
+ summary=['TLDR'],
+ summary_must_be_edited=[''],
+ content=['HEY WHY'],
+ status=['Accepted'],
+ owner=['daisy@example.com'],
+ label=['label-One', 'label-Two', 'RestrictedEnumField-7'],
+ custom_1=['NO'],
+ custom_2=['MOOD'],
+ custom_7=['37'],
+ components=['BackEnd'],
+ component_required=['on'],
+ owner_defaults_to_member=['on'],
+ add_approvals=['no'],
+ phase_0=[''],
+ phase_1=[''],
+ phase_2=[''],
+ phase_3=[''],
+ phase_4=[''],
+ phase_5=['OOPs'],
+ approval_3=['phase_0'],
+ approval_4=['phase_2'])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+
+ self.assertTrue('/templates/detail?saved=1&template=TestTemplate&' in url)
+
+ self.services.template.UpdateIssueTemplateDef.assert_called_once_with(
+ self.mr.cnxn,
+ 47925,
+ 12345,
+ status='Accepted',
+ component_required=True,
+ phases=[],
+ approval_values=[],
+ name='TestTemplate',
+ field_values=[
+ tracker_pb2.FieldValue(field_id=1, str_value='NO', derived=False),
+ tracker_pb2.FieldValue(field_id=2, str_value='MOOD', derived=False),
+ tracker_pb2.FieldValue(field_id=7, int_value=37, derived=False),
+ tracker_pb2.FieldValue(field_id=9, int_value=3737, derived=False)
+ ],
+ labels=[
+ 'label-One', 'label-Two', 'RestrictedEnumField-7',
+ 'RestrictedEnumField_2-b'
+ ],
+ owner_defaults_to_member=True,
+ admin_ids=[],
+ content='HEY WHY',
+ component_ids=[1],
+ summary_must_be_edited=False,
+ summary='TLDR',
+ members_only=True,
+ owner_id=333)
+
+ def testProcessFormData_AcceptPhases(self):
+ self.mr.auth.effective_ids = {222}
+ post_data = fake.PostData(
+ name=['TestTemplate'],
+ members_only=['on'],
+ summary=['TLDR'],
+ summary_must_be_edited=[''],
+ content=['HEY WHY'],
+ status=['Accepted'],
+ owner=['daisy@example.com'],
+ label=['label-One', 'label-Two'],
+ custom_1=['NO'],
+ custom_2=['MOOD'],
+ components=['BackEnd'],
+ component_required=['on'],
+ owner_defaults_to_member=['on'],
+ add_approvals = ['on'],
+ phase_0=['Canary'],
+ phase_1=['Stable'],
+ phase_2=[''],
+ phase_3=[''],
+ phase_4=[''],
+ phase_5=[''],
+ approval_3=['phase_0'],
+ approval_4=['phase_1']
+ )
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+
+ self.assertTrue('/templates/detail?saved=1&template=TestTemplate&' in url)
+
+ self.services.template.UpdateIssueTemplateDef.assert_called_once_with(
+ self.mr.cnxn, 47925, 12345, status='Accepted', component_required=True,
+ phases=[
+ tracker_pb2.Phase(name='Canary', rank=0, phase_id=0),
+ tracker_pb2.Phase(name='Stable', rank=1, phase_id=1)],
+ approval_values=[tracker_pb2.ApprovalValue(approval_id=3, phase_id=0),
+ tracker_pb2.ApprovalValue(approval_id=4, phase_id=1)],
+ name='TestTemplate', field_values=[
+ tracker_pb2.FieldValue(field_id=1, str_value='NO', derived=False),
+ tracker_pb2.FieldValue(
+ field_id=2, str_value='MOOD', derived=False)],
+ labels=['label-One', 'label-Two'], owner_defaults_to_member=True,
+ admin_ids=[], content='HEY WHY', component_ids=[1],
+ summary_must_be_edited=False, summary='TLDR', members_only=True,
+ owner_id=333)
+
+ def testProcessFormData_Delete(self):
+ post_data = fake.PostData(
+ deletetemplate=['Submit'],
+ name=['TestTemplate'],
+ members_only=['on'],
+ )
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+
+ self.assertTrue('/p/None/adminTemplates?deleted=1' in url)
+ self.services.template.DeleteIssueTemplateDef\
+ .assert_called_once_with(self.mr.cnxn, 47925, 12345)
diff --git a/tracker/test/tracker_bizobj_test.py b/tracker/test/tracker_bizobj_test.py
new file mode 100644
index 0000000..29351b0
--- /dev/null
+++ b/tracker/test/tracker_bizobj_test.py
@@ -0,0 +1,2456 @@
+# Copyright 2016 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 issue bizobj functions."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+import logging
+
+from framework import framework_constants
+from framework import framework_views
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+
+
+class BizobjTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.services = service_manager.Services(
+ issue=fake.IssueService())
+ self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ self.config.field_defs = [
+ tracker_pb2.FieldDef(
+ field_id=1, project_id=789, field_name='EstDays',
+ field_type=tracker_pb2.FieldTypes.INT_TYPE)
+ ]
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, project_id=789, path='UI'),
+ tracker_pb2.ComponentDef(component_id=2, project_id=789, path='DB'),
+ ]
+
+ def testGetOwnerId(self):
+ issue = tracker_pb2.Issue()
+ self.assertEqual(
+ tracker_bizobj.GetOwnerId(issue), framework_constants.NO_USER_SPECIFIED)
+
+ issue.derived_owner_id = 123
+ self.assertEqual(tracker_bizobj.GetOwnerId(issue), 123)
+
+ issue.owner_id = 456
+ self.assertEqual(tracker_bizobj.GetOwnerId(issue), 456)
+
+ def testGetStatus(self):
+ issue = tracker_pb2.Issue()
+ self.assertEqual(tracker_bizobj.GetStatus(issue), '')
+
+ issue.derived_status = 'InReview'
+ self.assertEqual(tracker_bizobj.GetStatus(issue), 'InReview')
+
+ issue.status = 'Forgotten'
+ self.assertEqual(tracker_bizobj.GetStatus(issue), 'Forgotten')
+
+ def testGetCcIds(self):
+ issue = tracker_pb2.Issue()
+ self.assertEqual(tracker_bizobj.GetCcIds(issue), [])
+
+ issue.derived_cc_ids.extend([1, 2, 3])
+ self.assertEqual(tracker_bizobj.GetCcIds(issue), [1, 2, 3])
+
+ issue.cc_ids.extend([4, 5, 6])
+ self.assertEqual(tracker_bizobj.GetCcIds(issue), [4, 5, 6, 1, 2, 3])
+
+ def testGetApproverIds(self):
+ issue = tracker_pb2.Issue()
+ self.assertEqual(tracker_bizobj.GetApproverIds(issue), [])
+
+ av_1 = tracker_pb2.ApprovalValue(approver_ids=[111, 222])
+ av_2 = tracker_pb2.ApprovalValue()
+ av_3 = tracker_pb2.ApprovalValue(approver_ids=[222, 333])
+ issue.approval_values = [av_1, av_2, av_3]
+ self.assertItemsEqual(
+ tracker_bizobj.GetApproverIds(issue), [111, 222, 333])
+
+ def testGetLabels(self):
+ issue = tracker_pb2.Issue()
+ self.assertEqual(tracker_bizobj.GetLabels(issue), [])
+
+ issue.derived_labels.extend(['a', 'b', 'c'])
+ self.assertEqual(tracker_bizobj.GetLabels(issue), ['a', 'b', 'c'])
+
+ issue.labels.extend(['d', 'e', 'f'])
+ self.assertEqual(
+ tracker_bizobj.GetLabels(issue), ['d', 'e', 'f', 'a', 'b', 'c'])
+
+ def testFindFieldDef_None(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ self.assertIsNone(tracker_bizobj.FindFieldDef(None, config))
+
+ def testFindFieldDef_Empty(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ self.assertIsNone(tracker_bizobj.FindFieldDef('EstDays', config))
+
+ def testFindFieldDef_Default(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ self.assertIsNone(tracker_bizobj.FindFieldDef('EstDays', config))
+
+ def testFindFieldDef_Normal(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ fd = tracker_pb2.FieldDef(field_name='EstDays')
+ config.field_defs = [fd]
+ self.assertEqual(fd, tracker_bizobj.FindFieldDef('EstDays', config))
+ self.assertEqual(fd, tracker_bizobj.FindFieldDef('ESTDAYS', config))
+ self.assertIsNone(tracker_bizobj.FindFieldDef('Unknown', config))
+
+ def testFindFieldDefByID_Empty(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ self.assertIsNone(tracker_bizobj.FindFieldDefByID(1, config))
+
+ def testFindFieldDefByID_Default(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ self.assertIsNone(tracker_bizobj.FindFieldDefByID(1, config))
+
+ def testFindFieldDefByID_Normal(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ fd = tracker_pb2.FieldDef(field_id=1)
+ config.field_defs = [fd]
+ self.assertEqual(fd, tracker_bizobj.FindFieldDefByID(1, config))
+ self.assertIsNone(tracker_bizobj.FindFieldDefByID(99, config))
+
+ def testFindApprovalDef_Empty(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ self.assertEqual(None, tracker_bizobj.FindApprovalDef(
+ 'Nonexistent', config))
+
+ def testFindApprovalDef_Normal(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ approval_fd = tracker_pb2.FieldDef(field_id=1, field_name='UIApproval')
+ approval_def = tracker_pb2.ApprovalDef(
+ approval_id=1, approver_ids=[111], survey='')
+ config.field_defs = [approval_fd]
+ config.approval_defs = [approval_def]
+ self.assertEqual(approval_def, tracker_bizobj.FindApprovalDef(
+ 'UIApproval', config))
+
+ def testFindApprovalDef_NotApproval(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ field_def = tracker_pb2.FieldDef(field_id=1, field_name='DesignDoc')
+ config.field_defs = [field_def]
+ self.assertEqual(None, tracker_bizobj.FindApprovalDef('DesignDoc', config))
+
+ def testFindApprovalDefByID_Empty(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ self.assertEqual(None, tracker_bizobj.FindApprovalDefByID(1, config))
+
+ def testFindApprovalDefByID_Normal(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ approval_def = tracker_pb2.ApprovalDef(
+ approval_id=1, approver_ids=[111, 222], survey='')
+ config.approval_defs = [approval_def]
+ self.assertEqual(approval_def, tracker_bizobj.FindApprovalDefByID(
+ 1, config))
+ self.assertEqual(None, tracker_bizobj.FindApprovalDefByID(99, config))
+
+ def testFindApprovalValueByID_Normal(self):
+ av_24 = tracker_pb2.ApprovalValue(approval_id=24)
+ av_22 = tracker_pb2.ApprovalValue()
+ self.assertEqual(
+ av_24, tracker_bizobj.FindApprovalValueByID(24, [av_22, av_24]))
+
+ def testFindApprovalValueByID_None(self):
+ av_no_id = tracker_pb2.ApprovalValue()
+ self.assertIsNone(tracker_bizobj.FindApprovalValueByID(24, [av_no_id]))
+
+ def testFindApprovalsSubfields(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ subfd_1 = tracker_pb2.FieldDef(approval_id=1)
+ subfd_2 = tracker_pb2.FieldDef(approval_id=2)
+ subfd_3 = tracker_pb2.FieldDef(approval_id=1)
+ subfd_4 = tracker_pb2.FieldDef()
+ config.field_defs = [subfd_1, subfd_2, subfd_3, subfd_4]
+
+ subfields_dict = tracker_bizobj.FindApprovalsSubfields([1, 2], config)
+ self.assertItemsEqual(subfields_dict[1], [subfd_1, subfd_3])
+ self.assertItemsEqual(subfields_dict[2], [subfd_2])
+ self.assertItemsEqual(subfields_dict[3], [])
+
+ def testFindPhaseByID_Normal(self):
+ canary_phase = tracker_pb2.Phase(phase_id=2, name='Canary')
+ stable_phase = tracker_pb2.Phase(name='Stable')
+ self.assertEqual(
+ canary_phase,
+ tracker_bizobj.FindPhaseByID(2, [stable_phase, canary_phase]))
+
+ def testFindPhaseByID_None(self):
+ stable_phase = tracker_pb2.Phase(name='Stable')
+ self.assertIsNone(tracker_bizobj.FindPhaseByID(42, [stable_phase]))
+
+ def testFindPhase_Normal(self):
+ canary_phase = tracker_pb2.Phase(phase_id=2)
+ stable_phase = tracker_pb2.Phase(name='Stable')
+ self.assertEqual(stable_phase, tracker_bizobj.FindPhase(
+ 'Stable', [stable_phase, canary_phase]))
+
+ def testFindPhase_None(self):
+ self.assertIsNone(tracker_bizobj.FindPhase('ghost_phase', []))
+
+ def testGetGrantedPerms_Empty(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ issue = tracker_pb2.Issue()
+ self.assertEqual(
+ set(), tracker_bizobj.GetGrantedPerms(issue, {111}, config))
+
+ def testGetGrantedPerms_Default(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ issue = tracker_pb2.Issue()
+ self.assertEqual(
+ set(), tracker_bizobj.GetGrantedPerms(issue, {111}, config))
+
+ def testGetGrantedPerms_NothingGranted(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ fd = tracker_pb2.FieldDef(field_id=1) # Nothing granted
+ config.field_defs = [fd]
+ fv = tracker_pb2.FieldValue(field_id=1, user_id=222)
+ issue = tracker_pb2.Issue(field_values=[fv])
+ self.assertEqual(
+ set(),
+ tracker_bizobj.GetGrantedPerms(issue, {111, 222}, config))
+
+ def testGetGrantedPerms_Normal(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ fd = tracker_pb2.FieldDef(field_id=1, grants_perm='Highlight')
+ config.field_defs = [fd]
+ fv = tracker_pb2.FieldValue(field_id=1, user_id=222)
+ issue = tracker_pb2.Issue(field_values=[fv])
+ self.assertEqual(
+ set(),
+ tracker_bizobj.GetGrantedPerms(issue, {111}, config))
+ self.assertEqual(
+ set(['highlight']),
+ tracker_bizobj.GetGrantedPerms(issue, {111, 222}, config))
+
+ def testLabelsByPrefix(self):
+ expected = tracker_bizobj.LabelsByPrefix(
+ ['OneWordLabel', 'Key-Value1', 'Key-Value2', 'Launch-X-Y-Z'],
+ ['launch-x'])
+ self.assertEqual(
+ {'key': ['Value1', 'Value2'],
+ 'launch-x': ['Y-Z']},
+ expected)
+
+ def testLabelIsMaskedByField(self):
+ self.assertIsNone(tracker_bizobj.LabelIsMaskedByField('UI', []))
+ self.assertIsNone(tracker_bizobj.LabelIsMaskedByField('P-1', []))
+ field_names = ['priority', 'size']
+ self.assertIsNone(tracker_bizobj.LabelIsMaskedByField(
+ 'UI', field_names))
+ self.assertIsNone(tracker_bizobj.LabelIsMaskedByField(
+ 'OS-All', field_names))
+ self.assertEqual(
+ 'size', tracker_bizobj.LabelIsMaskedByField('size-xl', field_names))
+ self.assertEqual(
+ 'size', tracker_bizobj.LabelIsMaskedByField('Size-XL', field_names))
+
+ def testNonMaskedLabels(self):
+ self.assertEqual([], tracker_bizobj.NonMaskedLabels([], []))
+ field_names = ['priority', 'size']
+ self.assertEqual([], tracker_bizobj.NonMaskedLabels([], field_names))
+ self.assertEqual(
+ [], tracker_bizobj.NonMaskedLabels(['Size-XL'], field_names))
+ self.assertEqual(
+ ['Hot'], tracker_bizobj.NonMaskedLabels(['Hot'], field_names))
+ self.assertEqual(
+ ['Hot'],
+ tracker_bizobj.NonMaskedLabels(['Hot', 'Size-XL'], field_names))
+
+ def testMakeApprovalValue_Basic(self):
+ av = tracker_bizobj.MakeApprovalValue(2)
+ expected = tracker_pb2.ApprovalValue(approval_id=2)
+ self.assertEqual(av, expected)
+
+ def testMakeApprovalValue_Full(self):
+ av = tracker_bizobj.MakeApprovalValue(
+ 2, approver_ids=[], status=tracker_pb2.ApprovalStatus.APPROVED,
+ setter_id=3, set_on=123, phase_id=3)
+ expected = tracker_pb2.ApprovalValue(
+ approval_id=2, approver_ids=[],
+ status=tracker_pb2.ApprovalStatus.APPROVED,
+ setter_id=3, set_on=123, phase_id=3)
+ self.assertEqual(av, expected)
+
+ def testMakeFieldDef_Basic(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 1, 789, 'Size', tracker_pb2.FieldTypes.USER_TYPE, None, None,
+ False, False, False, None, None, None, False,
+ None, None, None, 'no_action', 'Some field', False)
+ self.assertEqual(1, fd.field_id)
+ self.assertEqual(None, fd.approval_id)
+ self.assertFalse(fd.is_phase_field)
+ self.assertFalse(fd.is_restricted_field)
+
+ def testMakeFieldDef_Full(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 1,
+ 789,
+ 'Size',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ None,
+ False,
+ False,
+ False,
+ 1,
+ 100,
+ None,
+ False,
+ None,
+ None,
+ None,
+ 'no_action',
+ 'Some field',
+ False,
+ approval_id=4,
+ is_phase_field=True,
+ is_restricted_field=True)
+ self.assertEqual(1, fd.min_value)
+ self.assertEqual(100, fd.max_value)
+ self.assertEqual(4, fd.approval_id)
+ self.assertTrue(fd.is_phase_field)
+ self.assertTrue(fd.is_restricted_field)
+
+ fd = tracker_bizobj.MakeFieldDef(
+ 1,
+ 789,
+ 'Size',
+ tracker_pb2.FieldTypes.STR_TYPE,
+ None,
+ None,
+ False,
+ False,
+ False,
+ None,
+ None,
+ 'A.*Z',
+ False,
+ 'EditIssue',
+ None,
+ None,
+ 'no_action',
+ 'Some field',
+ False,
+ 4,
+ is_restricted_field=False)
+ self.assertEqual('A.*Z', fd.regex)
+ self.assertEqual('EditIssue', fd.needs_perm)
+ self.assertEqual(4, fd.approval_id)
+ self.assertFalse(fd.is_restricted_field)
+
+ def testMakeFieldDef_IntBools(self):
+ fd = tracker_bizobj.MakeFieldDef(
+ 1,
+ 789,
+ 'Size',
+ tracker_pb2.FieldTypes.INT_TYPE,
+ None,
+ None,
+ 0,
+ 0,
+ 0,
+ 1,
+ 100,
+ None,
+ 0,
+ None,
+ None,
+ None,
+ 'no_action',
+ 'Some field',
+ 0,
+ approval_id=4,
+ is_phase_field=1,
+ is_restricted_field=1)
+ self.assertFalse(fd.is_required)
+ self.assertFalse(fd.is_niche)
+ self.assertFalse(fd.is_multivalued)
+ self.assertFalse(fd.needs_member)
+ self.assertFalse(fd.is_deleted)
+ self.assertTrue(fd.is_phase_field)
+ self.assertTrue(fd.is_restricted_field)
+
+ def testMakeFieldValue(self):
+ # Only the first value counts.
+ fv = tracker_bizobj.MakeFieldValue(1, 42, 'yay', 111, None, None, True)
+ self.assertEqual(1, fv.field_id)
+ self.assertEqual(42, fv.int_value)
+ self.assertIsNone(fv.str_value)
+ self.assertEqual(None, fv.user_id)
+ self.assertEqual(None, fv.phase_id)
+
+ fv = tracker_bizobj.MakeFieldValue(1, None, 'yay', 111, None, None, True)
+ self.assertEqual('yay', fv.str_value)
+ self.assertEqual(None, fv.user_id)
+
+ fv = tracker_bizobj.MakeFieldValue(1, None, None, 111, None, None, True)
+ self.assertEqual(111, fv.user_id)
+ self.assertEqual(True, fv.derived)
+
+ fv = tracker_bizobj.MakeFieldValue(
+ 1, None, None, None, 1234567890, None, True)
+ self.assertEqual(1234567890, fv.date_value)
+ self.assertEqual(True, fv.derived)
+
+ fv = tracker_bizobj.MakeFieldValue(
+ 1, None, None, None, None, 'www.google.com', True, phase_id=1)
+ self.assertEqual('www.google.com', fv.url_value)
+ self.assertEqual(True, fv.derived)
+ self.assertEqual(1, fv.phase_id)
+
+ with self.assertRaises(ValueError):
+ tracker_bizobj.MakeFieldValue(1, None, None, None, None, None, True)
+
+ def testGetFieldValueWithRawValue(self):
+ class MockUser(object):
+ def __init__(self):
+ self.email = 'test@example.com'
+ users_by_id = {111: MockUser()}
+
+ class MockFieldValue(object):
+ def __init__(
+ self, int_value=None, str_value=None, user_id=None,
+ date_value=None, url_value=None):
+ self.int_value = int_value
+ self.str_value = str_value
+ self.user_id = user_id
+ self.date_value = date_value
+ self.url_value = url_value
+
+ # Test user types.
+ # Use user_id from the field_value and get user from users_by_id.
+ val = tracker_bizobj.GetFieldValueWithRawValue(
+ field_type=tracker_pb2.FieldTypes.USER_TYPE,
+ users_by_id=users_by_id,
+ field_value=MockFieldValue(user_id=111),
+ raw_value=113,
+ )
+ self.assertEqual('test@example.com', val)
+ # Specify user_id that does not exist in users_by_id.
+ val = tracker_bizobj.GetFieldValueWithRawValue(
+ field_type=tracker_pb2.FieldTypes.USER_TYPE,
+ users_by_id=users_by_id,
+ field_value=MockFieldValue(user_id=112),
+ raw_value=113,
+ )
+ self.assertEqual(112, val)
+ # Pass in empty users_by_id.
+ val = tracker_bizobj.GetFieldValueWithRawValue(
+ field_type=tracker_pb2.FieldTypes.USER_TYPE,
+ users_by_id={},
+ field_value=MockFieldValue(user_id=111),
+ raw_value=113,
+ )
+ self.assertEqual(111, val)
+ # Test different raw_values.
+ raw_value_tests = (
+ (111, 'test@example.com'),
+ (112, 112),
+ (framework_constants.NO_USER_NAME, framework_constants.NO_USER_NAME))
+ for (raw_value, expected_output) in raw_value_tests:
+ val = tracker_bizobj.GetFieldValueWithRawValue(
+ field_type=tracker_pb2.FieldTypes.USER_TYPE,
+ users_by_id=users_by_id,
+ field_value=None,
+ raw_value=raw_value,
+ )
+ self.assertEqual(expected_output, val)
+
+ # Test enum types.
+ # The returned value should be the raw_value regardless of field_value being
+ # specified.
+ for field_value in (MockFieldValue(), None):
+ val = tracker_bizobj.GetFieldValueWithRawValue(
+ field_type=tracker_pb2.FieldTypes.ENUM_TYPE,
+ users_by_id=users_by_id,
+ field_value=field_value,
+ raw_value='abc',
+ )
+ self.assertEqual('abc', val)
+
+ # Test int type.
+ # Use int_value from the field_value.
+ val = tracker_bizobj.GetFieldValueWithRawValue(
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ users_by_id=users_by_id,
+ field_value=MockFieldValue(int_value=100),
+ raw_value=101,
+ )
+ self.assertEqual(100, val)
+ # Use the raw_value when field_value is not specified.
+ val = tracker_bizobj.GetFieldValueWithRawValue(
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ users_by_id=users_by_id,
+ field_value=None,
+ raw_value=101,
+ )
+ self.assertEqual(101, val)
+
+ # Test str type.
+ # Use str_value from the field_value.
+ val = tracker_bizobj.GetFieldValueWithRawValue(
+ field_type=tracker_pb2.FieldTypes.STR_TYPE,
+ users_by_id=users_by_id,
+ field_value=MockFieldValue(str_value='testing'),
+ raw_value='test',
+ )
+ self.assertEqual('testing', val)
+ # Use the raw_value when field_value is not specified.
+ val = tracker_bizobj.GetFieldValueWithRawValue(
+ field_type=tracker_pb2.FieldTypes.STR_TYPE,
+ users_by_id=users_by_id,
+ field_value=None,
+ raw_value='test',
+ )
+ self.assertEqual('test', val)
+
+ # Test date type.
+ # Use date_value from the field_value.
+ val = tracker_bizobj.GetFieldValueWithRawValue(
+ field_type=tracker_pb2.FieldTypes.DATE_TYPE,
+ users_by_id=users_by_id,
+ field_value=MockFieldValue(date_value=1234567890),
+ raw_value=2345678901,
+ )
+ self.assertEqual('2009-02-13', val)
+ # Use the raw_value when field_value is not specified.
+ val = tracker_bizobj.GetFieldValueWithRawValue(
+ field_type=tracker_pb2.FieldTypes.DATE_TYPE,
+ users_by_id=users_by_id,
+ field_value=None,
+ raw_value='2016-10-30',
+ )
+ self.assertEqual('2016-10-30', val)
+
+ def testFindComponentDef_Empty(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ actual = tracker_bizobj.FindComponentDef('DB', config)
+ self.assertIsNone(actual)
+
+ def testFindComponentDef_NoMatch(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ cd = tracker_pb2.ComponentDef(path='UI>Splash')
+ config.component_defs.append(cd)
+ actual = tracker_bizobj.FindComponentDef('DB', config)
+ self.assertIsNone(actual)
+
+ def testFindComponentDef_MatchFound(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ cd = tracker_pb2.ComponentDef(path='UI>Splash')
+ config.component_defs.append(cd)
+ actual = tracker_bizobj.FindComponentDef('UI>Splash', config)
+ self.assertEqual(cd, actual)
+
+ def testFindMatchingComponentIDs_Empty(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ actual = tracker_bizobj.FindMatchingComponentIDs('DB', config)
+ self.assertEqual([], actual)
+ actual = tracker_bizobj.FindMatchingComponentIDs('DB', config, exact=False)
+ self.assertEqual([], actual)
+
+ def testFindMatchingComponentIDs_NoMatch(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ config.component_defs.append(tracker_pb2.ComponentDef(
+ component_id=1, path='UI>Splash'))
+ config.component_defs.append(tracker_pb2.ComponentDef(
+ component_id=2, path='UI>AboutBox'))
+ actual = tracker_bizobj.FindMatchingComponentIDs('DB', config)
+ self.assertEqual([], actual)
+ actual = tracker_bizobj.FindMatchingComponentIDs('DB', config, exact=False)
+ self.assertEqual([], actual)
+
+ def testFindMatchingComponentIDs_Match(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ config.component_defs.append(tracker_pb2.ComponentDef(
+ component_id=1, path='UI>Splash'))
+ config.component_defs.append(tracker_pb2.ComponentDef(
+ component_id=2, path='UI>AboutBox'))
+ config.component_defs.append(tracker_pb2.ComponentDef(
+ component_id=3, path='DB>Attachments'))
+ actual = tracker_bizobj.FindMatchingComponentIDs('DB', config)
+ self.assertEqual([], actual)
+ actual = tracker_bizobj.FindMatchingComponentIDs('DB', config, exact=False)
+ self.assertEqual([3], actual)
+
+ def testFindMatchingComponentIDs_MatchMultiple(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ config.component_defs.append(tracker_pb2.ComponentDef(
+ component_id=1, path='UI>Splash'))
+ config.component_defs.append(tracker_pb2.ComponentDef(
+ component_id=2, path='UI>AboutBox'))
+ config.component_defs.append(tracker_pb2.ComponentDef(
+ component_id=22, path='UI>AboutBox'))
+ config.component_defs.append(tracker_pb2.ComponentDef(
+ component_id=3, path='DB>Attachments'))
+ actual = tracker_bizobj.FindMatchingComponentIDs('UI>AboutBox', config)
+ self.assertEqual([2, 22], actual)
+ actual = tracker_bizobj.FindMatchingComponentIDs('UI', config, exact=False)
+ self.assertEqual([1, 2, 22], actual)
+
+ def testFindComponentDefByID_Empty(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ actual = tracker_bizobj.FindComponentDefByID(999, config)
+ self.assertIsNone(actual)
+
+ def testFindComponentDefByID_NoMatch(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ config.component_defs.append(tracker_pb2.ComponentDef(
+ component_id=1, path='UI>Splash'))
+ config.component_defs.append(tracker_pb2.ComponentDef(
+ component_id=2, path='UI>AboutBox'))
+ actual = tracker_bizobj.FindComponentDefByID(999, config)
+ self.assertIsNone(actual)
+
+ def testFindComponentDefByID_MatchFound(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ cd = tracker_pb2.ComponentDef(component_id=1, path='UI>Splash')
+ config.component_defs.append(cd)
+ config.component_defs.append(tracker_pb2.ComponentDef(
+ component_id=2, path='UI>AboutBox'))
+ actual = tracker_bizobj.FindComponentDefByID(1, config)
+ self.assertEqual(cd, actual)
+
+ def testFindAncestorComponents_Empty(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ cd = tracker_pb2.ComponentDef(component_id=1, path='UI>Splash')
+ actual = tracker_bizobj.FindAncestorComponents(config, cd)
+ self.assertEqual([], actual)
+
+ def testFindAncestorComponents_NoMatch(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ cd = tracker_pb2.ComponentDef(component_id=1, path='UI>Splash')
+ config.component_defs.append(tracker_pb2.ComponentDef(
+ component_id=2, path='UI>AboutBox'))
+ actual = tracker_bizobj.FindAncestorComponents(config, cd)
+ self.assertEqual([], actual)
+
+ def testFindAncestorComponents_NoComponents(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ cd = tracker_pb2.ComponentDef(component_id=1, path='UI')
+ config.component_defs.append(cd)
+ cd2 = tracker_pb2.ComponentDef(component_id=2, path='UI>Splash')
+ config.component_defs.append(cd2)
+ actual = tracker_bizobj.FindAncestorComponents(config, cd2)
+ self.assertEqual([cd], actual)
+
+ def testGetIssueComponentsAndAncestors_NoSuchComponent(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ cd = tracker_pb2.ComponentDef(component_id=1, path='UI')
+ config.component_defs.append(cd)
+ cd2 = tracker_pb2.ComponentDef(component_id=2, path='UI>Splash')
+ config.component_defs.append(cd2)
+ issue = tracker_pb2.Issue(component_ids=[999])
+ actual = tracker_bizobj.GetIssueComponentsAndAncestors(issue, config)
+ self.assertEqual([], actual)
+
+ def testGetIssueComponentsAndAncestors_AffectsNoComponents(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ cd = tracker_pb2.ComponentDef(component_id=1, path='UI')
+ config.component_defs.append(cd)
+ cd2 = tracker_pb2.ComponentDef(component_id=2, path='UI>Splash')
+ config.component_defs.append(cd2)
+ issue = tracker_pb2.Issue(component_ids=[])
+ actual = tracker_bizobj.GetIssueComponentsAndAncestors(issue, config)
+ self.assertEqual([], actual)
+
+ def testGetIssueComponentsAndAncestors_AffectsSomeComponents(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ cd = tracker_pb2.ComponentDef(component_id=1, path='UI')
+ config.component_defs.append(cd)
+ cd2 = tracker_pb2.ComponentDef(component_id=2, path='UI>Splash')
+ config.component_defs.append(cd2)
+ issue = tracker_pb2.Issue(component_ids=[2])
+ actual = tracker_bizobj.GetIssueComponentsAndAncestors(issue, config)
+ self.assertEqual([cd, cd2], actual)
+
+ def testFindDescendantComponents_Empty(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ cd = tracker_pb2.ComponentDef(component_id=1, path='UI')
+ actual = tracker_bizobj.FindDescendantComponents(config, cd)
+ self.assertEqual([], actual)
+
+ def testFindDescendantComponents_NoMatch(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ cd = tracker_pb2.ComponentDef(component_id=1, path='UI')
+ config.component_defs.append(cd)
+ actual = tracker_bizobj.FindDescendantComponents(config, cd)
+ self.assertEqual([], actual)
+
+ def testFindDescendantComponents_SomeMatch(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ cd = tracker_pb2.ComponentDef(component_id=1, path='UI')
+ config.component_defs.append(cd)
+ cd2 = tracker_pb2.ComponentDef(component_id=2, path='UI>Splash')
+ config.component_defs.append(cd2)
+ actual = tracker_bizobj.FindDescendantComponents(config, cd)
+ self.assertEqual([cd2], actual)
+
+ def testMakeComponentDef(self):
+ cd = tracker_bizobj.MakeComponentDef(
+ 1, 789, 'UI', 'doc', False, [111], [222], 1234567890,
+ 111)
+ self.assertEqual(1, cd.component_id)
+ self.assertEqual([111], cd.admin_ids)
+ self.assertEqual([], cd.label_ids)
+
+ def testMakeSavedQuery_WithNone(self):
+ sq = tracker_bizobj.MakeSavedQuery(
+ None, 'my query', 2, 'priority:high')
+ self.assertEqual(None, sq.query_id)
+ self.assertEqual(None, sq.subscription_mode)
+ self.assertEqual([], sq.executes_in_project_ids)
+
+ def testMakeSavedQuery(self):
+ sq = tracker_bizobj.MakeSavedQuery(
+ 100, 'my query', 2, 'priority:high',
+ subscription_mode='immediate', executes_in_project_ids=[789])
+ self.assertEqual(100, sq.query_id)
+ self.assertEqual('immediate', sq.subscription_mode)
+ self.assertEqual([789], sq.executes_in_project_ids)
+
+ def testConvertDictToTemplate(self):
+ template = tracker_bizobj.ConvertDictToTemplate(
+ dict(name='name', content='content', summary='summary',
+ status='status', owner_id=111))
+ self.assertEqual('name', template.name)
+ self.assertEqual('content', template.content)
+ self.assertEqual('summary', template.summary)
+ self.assertEqual('status', template.status)
+ self.assertEqual(111, template.owner_id)
+ self.assertFalse(template.summary_must_be_edited)
+ self.assertTrue(template.owner_defaults_to_member)
+ self.assertFalse(template.component_required)
+
+ template = tracker_bizobj.ConvertDictToTemplate(
+ dict(name='name', content='content', labels=['a', 'b', 'c']))
+ self.assertListEqual(
+ ['a', 'b', 'c'], list(template.labels))
+
+ template = tracker_bizobj.ConvertDictToTemplate(
+ dict(name='name', content='content', summary_must_be_edited=True,
+ owner_defaults_to_member=True, component_required=True))
+ self.assertTrue(template.summary_must_be_edited)
+ self.assertTrue(template.owner_defaults_to_member)
+ self.assertTrue(template.component_required)
+
+ template = tracker_bizobj.ConvertDictToTemplate(
+ dict(name='name', content='content', summary_must_be_edited=False,
+ owner_defaults_to_member=False, component_required=False))
+ self.assertFalse(template.summary_must_be_edited)
+ self.assertFalse(template.owner_defaults_to_member)
+ self.assertFalse(template.component_required)
+
+ def CheckDefaultConfig(self, config):
+ self.assertTrue(len(config.well_known_statuses) > 0)
+ self.assertTrue(config.statuses_offer_merge > 0)
+ self.assertTrue(len(config.well_known_labels) > 0)
+ self.assertTrue(len(config.exclusive_label_prefixes) > 0)
+ # TODO(jrobbins): test actual values from default config
+
+ def testMakeDefaultProjectIssueConfig(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ config.default_template_for_developers = 1
+ config.default_template_for_users = 2
+ self.CheckDefaultConfig(config)
+
+ def testHarmonizeConfigs_Empty(self):
+ harmonized = tracker_bizobj.HarmonizeConfigs([])
+ self.CheckDefaultConfig(harmonized)
+
+ def testHarmonizeConfigs(self):
+ c1 = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ harmonized = tracker_bizobj.HarmonizeConfigs([c1])
+ self.assertListEqual(
+ [stat.status for stat in c1.well_known_statuses],
+ [stat.status for stat in harmonized.well_known_statuses])
+ self.assertListEqual(
+ [lab.label for lab in c1.well_known_labels],
+ [lab.label for lab in harmonized.well_known_labels])
+ self.assertEqual('', harmonized.default_sort_spec)
+
+ c2 = tracker_bizobj.MakeDefaultProjectIssueConfig(678)
+ tracker_bizobj.SetConfigStatuses(c2, [
+ ('Unconfirmed', '', True, False),
+ ('New', '', True, True),
+ ('Accepted', '', True, False),
+ ('Begun', '', True, False),
+ ('Fixed', '', False, False),
+ ('Obsolete', '', False, False)])
+ tracker_bizobj.SetConfigLabels(c2, [
+ ('Pri-0', '', False),
+ ('Priority-High', '', True),
+ ('Pri-1', '', False),
+ ('Priority-Medium', '', True),
+ ('Pri-2', '', False),
+ ('Priority-Low', '', True),
+ ('Pri-3', '', False),
+ ('Pri-4', '', False)])
+ c2.default_sort_spec = 'Pri -status'
+
+ c1.approval_defs = [
+ tracker_pb2.ApprovalDef(approval_id=1),
+ tracker_pb2.ApprovalDef(approval_id=3),
+ ]
+ c1.field_defs = [
+ tracker_pb2.FieldDef(
+ field_id=1, project_id=789, field_name='CowApproval',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE),
+ tracker_pb2.FieldDef(
+ field_id=3, project_id=789, field_name='MooApproval',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE)
+ ]
+ c2.approval_defs = [
+ tracker_pb2.ApprovalDef(approval_id=2),
+ ]
+ c2.field_defs = [
+ tracker_pb2.FieldDef(
+ field_id=2, project_id=788, field_name='CowApproval',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE),
+ ]
+ harmonized = tracker_bizobj.HarmonizeConfigs([c1, c2])
+ result_statuses = [stat.status
+ for stat in harmonized.well_known_statuses]
+ result_labels = [lab.label
+ for lab in harmonized.well_known_labels]
+ self.assertListEqual(
+ ['Unconfirmed', 'New', 'Accepted', 'Begun', 'Started', 'Fixed',
+ 'Obsolete', 'Verified', 'Invalid', 'Duplicate', 'WontFix', 'Done'],
+ result_statuses)
+ self.assertListEqual(
+ ['Pri-0', 'Type-Defect', 'Type-Enhancement', 'Type-Task',
+ 'Type-Other', 'Priority-Critical', 'Priority-High',
+ 'Pri-1', 'Priority-Medium', 'Pri-2', 'Priority-Low', 'Pri-3',
+ 'Pri-4'],
+ result_labels[:result_labels.index('OpSys-All')])
+ self.assertEqual('Pri -status', harmonized.default_sort_spec.strip())
+ self.assertItemsEqual(c1.field_defs + c2.field_defs,
+ harmonized.field_defs)
+ self.assertItemsEqual(c1.approval_defs + c2.approval_defs,
+ harmonized.approval_defs)
+
+ def testHarmonizeConfigsMeansOpen(self):
+ c1 = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ c2 = tracker_bizobj.MakeDefaultProjectIssueConfig(678)
+ means_open = [("TT", True, True),
+ ("TF", True, False),
+ ("FT", False, True),
+ ("FF", False, False)]
+ tracker_bizobj.SetConfigStatuses(c1, [
+ (x[0], x[0], x[1], False)
+ for x in means_open])
+ tracker_bizobj.SetConfigStatuses(c2, [
+ (x[0], x[0], x[2], False)
+ for x in means_open])
+
+ harmonized = tracker_bizobj.HarmonizeConfigs([c1, c2])
+ for stat in harmonized.well_known_statuses:
+ self.assertEqual(stat.means_open, stat.status != "FF")
+
+ def testHarmonizeConfigs_DeletedCustomField(self):
+ """Only non-deleted custom fields in configs are included."""
+ harmonized = tracker_bizobj.HarmonizeConfigs([self.config])
+ self.assertEqual(1, len(harmonized.field_defs))
+
+ self.config.field_defs[0].is_deleted = True
+ harmonized = tracker_bizobj.HarmonizeConfigs([self.config])
+ self.assertEqual(0, len(harmonized.field_defs))
+
+ def testHarmonizeLabelOrStatusRows_Empty(self):
+ def_rows = []
+ actual = tracker_bizobj.HarmonizeLabelOrStatusRows(def_rows)
+ self.assertEqual([], actual)
+
+ def testHarmonizeLabelOrStatusRows_Normal(self):
+ def_rows = [
+ (100, 789, 1, 'Priority-High'),
+ (101, 789, 2, 'Priority-Normal'),
+ (103, 789, 3, 'Priority-Low'),
+ (199, 789, None, 'Monday'),
+ (200, 678, 1, 'Priority-High'),
+ (201, 678, 2, 'Priority-Medium'),
+ (202, 678, 3, 'Priority-Low'),
+ (299, 678, None, 'Hot'),
+ ]
+ actual = tracker_bizobj.HarmonizeLabelOrStatusRows(def_rows)
+ self.assertEqual(
+ [(199, None, 'Monday'),
+ (299, None, 'Hot'),
+ (200, 1, 'Priority-High'),
+ (100, 1, 'Priority-High'),
+ (101, 2, 'Priority-Normal'),
+ (201, 2, 'Priority-Medium'),
+ (202, 3, 'Priority-Low'),
+ (103, 3, 'Priority-Low')
+ ],
+ actual)
+
+ def testCombineOrderedLists_Empty(self):
+ self.assertEqual([], tracker_bizobj._CombineOrderedLists([]))
+
+ def testCombineOrderedLists_Normal(self):
+ a = ['Mon', 'Wed', 'Fri']
+ b = ['Mon', 'Tue']
+ c = ['Wed', 'Thu']
+ self.assertEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
+ tracker_bizobj._CombineOrderedLists([a, b, c]))
+
+ d = ['Mon', 'StartOfWeek', 'Wed', 'MidWeek', 'Fri', 'EndOfWeek']
+ self.assertEqual(['Mon', 'StartOfWeek', 'Tue', 'Wed', 'MidWeek', 'Thu',
+ 'Fri', 'EndOfWeek'],
+ tracker_bizobj._CombineOrderedLists([a, b, c, d]))
+
+ def testAccumulateCombinedList_Empty(self):
+ combined_items = []
+ combined_keys = []
+ seen_keys_set = set()
+ tracker_bizobj._AccumulateCombinedList(
+ [], combined_items, combined_keys, seen_keys_set)
+ self.assertEqual([], combined_items)
+ self.assertEqual([], combined_keys)
+ self.assertEqual(set(), seen_keys_set)
+
+ def testAccumulateCombinedList_Normal(self):
+ combined_items = ['a', 'b', 'C']
+ combined_keys = ['a', 'b', 'c'] # Keys are always lowercased
+ seen_keys_set = set(['a', 'b', 'c'])
+ tracker_bizobj._AccumulateCombinedList(
+ ['b', 'x', 'C', 'd', 'a'], combined_items, combined_keys, seen_keys_set)
+ self.assertEqual(['a', 'b', 'x', 'C', 'd'], combined_items)
+ self.assertEqual(['a', 'b', 'x', 'c', 'd'], combined_keys)
+ self.assertEqual(set(['a', 'b', 'x', 'c', 'd']), seen_keys_set)
+
+ def testAccumulateCombinedList_NormalWithKeyFunction(self):
+ combined_items = ['A', 'B', 'C']
+ combined_keys = ['@a', '@b', '@c']
+ seen_keys_set = set(['@a', '@b', '@c'])
+ tracker_bizobj._AccumulateCombinedList(
+ ['B', 'X', 'c', 'D', 'A'], combined_items, combined_keys, seen_keys_set,
+ key=lambda s: '@' + s)
+ self.assertEqual(['A', 'B', 'X', 'C', 'D'], combined_items)
+ self.assertEqual(['@a', '@b', '@x', '@c', '@d'], combined_keys)
+ self.assertEqual(set(['@a', '@b', '@x', '@c', '@d']), seen_keys_set)
+
+ def testGetBuiltInQuery(self):
+ self.assertEqual(
+ 'is:open', tracker_bizobj.GetBuiltInQuery(2))
+ self.assertEqual(
+ '', tracker_bizobj.GetBuiltInQuery(101))
+
+ def testUsersInvolvedInComment(self):
+ comment = tracker_pb2.IssueComment()
+ self.assertEqual({0}, tracker_bizobj.UsersInvolvedInComment(comment))
+
+ comment.user_id = 111
+ self.assertEqual(
+ {111}, tracker_bizobj.UsersInvolvedInComment(comment))
+
+ amendment = tracker_pb2.Amendment(newvalue='foo')
+ comment.amendments.append(amendment)
+ self.assertEqual(
+ {111}, tracker_bizobj.UsersInvolvedInComment(comment))
+
+ amendment.added_user_ids.append(222)
+ amendment.removed_user_ids.append(333)
+ self.assertEqual({111, 222, 333},
+ tracker_bizobj.UsersInvolvedInComment(comment))
+
+ def testUsersInvolvedInCommentList(self):
+ self.assertEqual(set(), tracker_bizobj.UsersInvolvedInCommentList([]))
+
+ c1 = tracker_pb2.IssueComment()
+ c1.user_id = 111
+ c1.amendments.append(tracker_pb2.Amendment(newvalue='foo'))
+
+ c2 = tracker_pb2.IssueComment()
+ c2.user_id = 111
+ c2.amendments.append(tracker_pb2.Amendment(
+ added_user_ids=[222], removed_user_ids=[333]))
+
+ self.assertEqual({111},
+ tracker_bizobj.UsersInvolvedInCommentList([c1]))
+
+ self.assertEqual({111, 222, 333},
+ tracker_bizobj.UsersInvolvedInCommentList([c2]))
+
+ self.assertEqual({111, 222, 333},
+ tracker_bizobj.UsersInvolvedInCommentList([c1, c2]))
+
+ def testUsersInvolvedInIssues_Empty(self):
+ self.assertEqual(set(), tracker_bizobj.UsersInvolvedInIssues([]))
+
+ def testUsersInvolvedInIssues_Normal(self):
+ av_1 = tracker_pb2.ApprovalValue(approver_ids=[666, 222, 444])
+ av_2 = tracker_pb2.ApprovalValue(approver_ids=[777], setter_id=888)
+ issue1 = tracker_pb2.Issue(
+ reporter_id=111, owner_id=222, cc_ids=[222, 333],
+ approval_values=[av_1, av_2])
+ issue2 = tracker_pb2.Issue(
+ reporter_id=333, owner_id=444, derived_cc_ids=[222, 444])
+ issue2.field_values = [tracker_pb2.FieldValue(user_id=555)]
+ self.assertEqual(
+ set([0, 111, 222, 333, 444, 555, 666, 777, 888]),
+ tracker_bizobj.UsersInvolvedInIssues([issue1, issue2]))
+
+ def testUsersInvolvedInTemplate_Empty(self):
+ template = tracker_bizobj.MakeIssueTemplate(
+ 'A report', 'Something went wrong', 'New', None, 'Look out!',
+ ['Priority-High'], [], [], [])
+ self.assertEqual(set(), tracker_bizobj.UsersInvolvedInTemplate(template))
+
+ def testUsersInvolvedInTemplate_Normal(self):
+ template = tracker_bizobj.MakeIssueTemplate(
+ 'A report', 'Something went wrong', 'New', 111, 'Look out!',
+ ['Priority-High'], [], [333, 444], [])
+ template.field_values = [
+ tracker_bizobj.MakeFieldValue(22, None, None, 222, None, None, False),
+ tracker_bizobj.MakeFieldValue(23, None, None, 333, None, None, False),
+ tracker_bizobj.MakeFieldValue(24, None, None, 222, None, None, False),
+ tracker_bizobj.MakeFieldValue(25, None, 'pop', None, None, None, False)]
+ template.approval_values = [
+ tracker_pb2.ApprovalValue(
+ approval_id=30, setter_id=666, approver_ids=[444, 555]),
+ tracker_pb2.ApprovalValue(approval_id=31),
+ ]
+ self.assertEqual(
+ {111, 333, 444, 222, 555, 666},
+ tracker_bizobj.UsersInvolvedInTemplate(template))
+
+ def testUsersInvolvedInTemplates_NoTemplates(self):
+ self.assertEqual(set(), tracker_bizobj.UsersInvolvedInTemplates([]))
+
+ def testUsersInvolvedInTemplates_Normal(self):
+ template1 = tracker_bizobj.MakeIssueTemplate(
+ 'A report', 'Something went wrong', 'New', 111, 'Look out!',
+ ['Priority-High'], [], [333, 444], [])
+ template1.field_values = [
+ tracker_bizobj.MakeFieldValue(22, None, None, 222, None, None, False)]
+
+ template2 = tracker_bizobj.MakeIssueTemplate(
+ 'dude', 'wheres my', 'New', 222, 'car', [], [], [999, 888], [])
+ template2.field_values = [
+ tracker_bizobj.MakeFieldValue(23, None, None, 333, None, None, False)]
+ template2.approval_values = [
+ tracker_pb2.ApprovalValue(
+ approval_id=30, setter_id=666, approver_ids=[444, 555]),
+ tracker_pb2.ApprovalValue(approval_id=31)]
+
+ self.assertEqual(
+ {111, 333, 444, 222, 555, 666, 888, 999},
+ tracker_bizobj.UsersInvolvedInTemplates([template1, template2]))
+
+ def testUsersInvolvedInApprovalDefs_Empty(self):
+ """There are no user IDs given empty inputs"""
+ actual = tracker_bizobj.UsersInvolvedInApprovalDefs([], [])
+ self.assertEqual(set(), actual)
+
+ def testsersInvolvedInApprovalDefs_Normal(self):
+ """We find user IDs mentioned in approval approvers and field admins"""
+ self.config.field_defs[0].admin_ids = [111, 222]
+ approval_def = tracker_pb2.ApprovalDef(
+ approval_id=1, approver_ids=[111, 333], survey='')
+ self.config.approval_defs = [approval_def]
+ actual = tracker_bizobj.UsersInvolvedInApprovalDefs(
+ [approval_def], [self.config.field_defs[0]])
+ self.assertEqual({111, 222, 333}, actual)
+
+ def testUsersInvolvedInConfig_Empty(self):
+ """There are no user IDs mentioned in a default config."""
+ actual = tracker_bizobj.UsersInvolvedInConfig(self.config)
+ self.assertEqual(set(), actual)
+
+ def testUsersInvolvedInConfig_Normal(self):
+ """We find user IDs mentioned components, fields, and approvals."""
+ self.config.component_defs[0].admin_ids = [111]
+ self.config.component_defs[0].cc_ids = [444]
+ self.config.field_defs[0].admin_ids = [111, 222]
+ approval_def = tracker_pb2.ApprovalDef(
+ approval_id=1, approver_ids=[111, 333], survey='')
+ self.config.approval_defs = [approval_def]
+ actual = tracker_bizobj.UsersInvolvedInConfig(self.config)
+ self.assertEqual({111, 222, 333, 444}, actual)
+
+ def testLabelIDsInvolvedInConfig_Empty(self):
+ """There are no label IDs mentioned in a default config."""
+ actual = tracker_bizobj.LabelIDsInvolvedInConfig(self.config)
+ self.assertEqual(set(), actual)
+
+ def testLabelIDsInvolvedInConfig_Normal(self):
+ """We find label IDs added by components."""
+ self.config.component_defs[0].label_ids = [1, 2, 3]
+ actual = tracker_bizobj.LabelIDsInvolvedInConfig(self.config)
+ self.assertEqual({1, 2, 3}, actual)
+
+ def testMakeApprovalDelta_AllSpecified(self):
+ added_fv = tracker_bizobj.MakeFieldValue(
+ 1, None, 'added str', None, None, None, False)
+ removed_fv = tracker_bizobj.MakeFieldValue(
+ 1, None, 'removed str', None, None, None, False)
+ clear_fvs = [24]
+ labels_add = ['ittly-bittly', 'piggly-wiggly']
+ labels_remove = ['golly-goops', 'whoopsie']
+ actual = tracker_bizobj.MakeApprovalDelta(
+ tracker_pb2.ApprovalStatus.APPROVED, 111, [222], [],
+ [added_fv], [removed_fv], clear_fvs, labels_add, labels_remove,
+ set_on=1234)
+ self.assertEqual(actual.status, tracker_pb2.ApprovalStatus.APPROVED)
+ self.assertEqual(actual.setter_id, 111)
+ self.assertEqual(actual.set_on, 1234)
+ self.assertEqual(actual.subfield_vals_add, [added_fv])
+ self.assertEqual(actual.subfield_vals_remove, [removed_fv])
+ self.assertEqual(actual.subfields_clear, clear_fvs)
+ self.assertEqual(actual.labels_add, labels_add)
+ self.assertEqual(actual.labels_remove, labels_remove)
+
+ def testMakeApprovalDelta_WithNones(self):
+ added_fv = tracker_bizobj.MakeFieldValue(
+ 1, None, 'added str', None, None, None, False)
+ removed_fv = tracker_bizobj.MakeFieldValue(
+ 1, None, 'removed str', None, None, None, False)
+ clear_fields = [2]
+ labels_add = ['ittly-bittly', 'piggly-wiggly']
+ labels_remove = ['golly-goops', 'whoopsie']
+ actual = tracker_bizobj.MakeApprovalDelta(
+ None, 111, [222], [],
+ [added_fv], [removed_fv], clear_fields,
+ labels_add, labels_remove)
+ self.assertIsNone(actual.status)
+ self.assertIsNone(actual.setter_id)
+ self.assertIsNone(actual.set_on)
+
+ def testMakeIssueDelta_AllSpecified(self):
+ added_fv = tracker_bizobj.MakeFieldValue(
+ 1, None, 'added str', None, None, None, False)
+ removed_fv = tracker_bizobj.MakeFieldValue(
+ 1, None, 'removed str', None, None, None, False)
+ actual = tracker_bizobj.MakeIssueDelta(
+ 'New', 111, [222], [333], [1], [2],
+ ['AddedLabel'], ['RemovedLabel'], [added_fv], [removed_fv],
+ [3], [78901], [78902], [78903], [78904], 78905,
+ 'New summary',
+ ext_blocked_on_add=['b/123', 'b/234'],
+ ext_blocked_on_remove=['b/345', 'b/456'],
+ ext_blocking_add=['b/567', 'b/678'],
+ ext_blocking_remove=['b/789', 'b/890'])
+ self.assertEqual('New', actual.status)
+ self.assertEqual(111, actual.owner_id)
+ self.assertEqual([222], actual.cc_ids_add)
+ self.assertEqual([333], actual.cc_ids_remove)
+ self.assertEqual([1], actual.comp_ids_add)
+ self.assertEqual([2], actual.comp_ids_remove)
+ self.assertEqual(['AddedLabel'], actual.labels_add)
+ self.assertEqual(['RemovedLabel'], actual.labels_remove)
+ self.assertEqual([added_fv], actual.field_vals_add)
+ self.assertEqual([removed_fv], actual.field_vals_remove)
+ self.assertEqual([3], actual.fields_clear)
+ self.assertEqual([78901], actual.blocked_on_add)
+ self.assertEqual([78902], actual.blocked_on_remove)
+ self.assertEqual([78903], actual.blocking_add)
+ self.assertEqual([78904], actual.blocking_remove)
+ self.assertEqual(78905, actual.merged_into)
+ self.assertEqual('New summary', actual.summary)
+ self.assertEqual(['b/123', 'b/234'], actual.ext_blocked_on_add)
+ self.assertEqual(['b/345', 'b/456'], actual.ext_blocked_on_remove)
+ self.assertEqual(['b/567', 'b/678'], actual.ext_blocking_add)
+ self.assertEqual(['b/789', 'b/890'], actual.ext_blocking_remove)
+
+ def testMakeIssueDelta_WithNones(self):
+ """None for status, owner_id, or summary does not set a value."""
+ actual = tracker_bizobj.MakeIssueDelta(
+ None, None, [], [], [], [],
+ [], [], [], [],
+ [], [], [], [], [], None,
+ None)
+ self.assertIsNone(actual.status)
+ self.assertIsNone(actual.owner_id)
+ self.assertIsNone(actual.merged_into)
+ self.assertIsNone(actual.summary)
+
+ def testApplyLabelChanges_RemoveAndAdd(self):
+ issue = tracker_pb2.Issue(
+ labels=['tobe-removed', 'tobe-notremoved', 'tobe-removed-2'])
+ amendment = tracker_bizobj.ApplyLabelChanges(
+ issue, self.config,
+ [u'tobe-added', 'to:be-added-2'],
+ [u'tobe-removed', u'to:be-removed-2'])
+ self.assertEqual(amendment, tracker_bizobj.MakeLabelsAmendment(
+ ['tobe-added', 'tobe-added-2'], ['tobe-removed', 'tobe-removed-2']))
+
+ def testApplyLabelChanges_RemoveInvalidLabel(self):
+ issue = tracker_pb2.Issue(labels=[])
+ amendment = tracker_bizobj.ApplyLabelChanges(
+ issue, self.config, [], [u'lost-car'])
+ self.assertIsNone(amendment)
+
+ def testApplyLabelChanges_NoChangesAfterMerge(self):
+ issue = tracker_pb2.Issue(labels=['lost-car'])
+ amendment = tracker_bizobj.ApplyLabelChanges(
+ issue, self.config, [u'lost-car'], [])
+ self.assertIsNone(amendment)
+
+ def testApplyLabelChanges_Empty(self):
+ issue = tracker_pb2.Issue(labels=[])
+ amendment = tracker_bizobj.ApplyLabelChanges(issue, self.config, [], [])
+ self.assertIsNone(amendment)
+
+ def testApplyFieldValueChanges(self):
+ self.config.field_defs = [
+ tracker_pb2.FieldDef(
+ field_id=1, project_id=789, field_name='EstDays',
+ field_type=tracker_pb2.FieldTypes.INT_TYPE),
+ tracker_pb2.FieldDef(
+ field_id=2, project_id=789, field_name='SleepHrs',
+ field_type=tracker_pb2.FieldTypes.INT_TYPE, is_phase_field=True),
+ tracker_pb2.FieldDef(
+ field_id=3, project_id=789, field_name='Chickens',
+ field_type=tracker_pb2.FieldTypes.STR_TYPE, is_phase_field=True,
+ is_multivalued=True),
+ ]
+ original_keep = [
+ tracker_pb2.FieldValue(field_id=3, str_value='bok', phase_id=45)]
+ original_replace = [
+ tracker_pb2.FieldValue(field_id=1, int_value=72),
+ tracker_pb2.FieldValue(field_id=2, int_value=88, phase_id=44)]
+ original_remove = [
+ tracker_pb2.FieldValue(field_id=3, str_value='removedbok', phase_id=45),
+ ]
+ issue = tracker_pb2.Issue(
+ phases=[
+ tracker_pb2.Phase(phase_id=45, name='high-school'),
+ tracker_pb2.Phase(phase_id=44, name='college')])
+ issue.field_values = original_keep + original_replace + original_remove
+
+ fvs_add_ignore = [
+ tracker_pb2.FieldValue(field_id=3, str_value='egg', phase_id=42)]
+ fvs_add = [
+ tracker_pb2.FieldValue(field_id=1, int_value=73), # replace
+ tracker_pb2.FieldValue(field_id=2, int_value=99, phase_id=44), #replace
+ tracker_pb2.FieldValue(field_id=2, int_value=100, phase_id=45), # added
+ # added
+ tracker_pb2.FieldValue(field_id=3, str_value='rooster', phase_id=45),
+ ]
+ fvs_remove = original_remove
+ fields_clear = []
+ amendments = tracker_bizobj.ApplyFieldValueChanges(
+ issue, self.config, fvs_add+fvs_add_ignore, fvs_remove, fields_clear)
+
+ self.assertEqual(
+ amendments,
+ [tracker_bizobj.MakeFieldAmendment(1, self.config, [73]),
+ tracker_bizobj.MakeFieldAmendment(
+ 2, self.config, [99], phase_name='college'),
+ tracker_bizobj.MakeFieldAmendment(
+ 2, self.config, [100], phase_name='high-school'),
+ tracker_bizobj.MakeFieldAmendment(
+ 3, self.config, ['rooster'], old_values=['removedbok'],
+ phase_name='high-school')])
+ self.assertEqual(issue.field_values, original_keep + fvs_add)
+
+ def testApplyIssueDelta_NoChange(self):
+ """A delta with no change should change nothing."""
+ issue = tracker_pb2.Issue(
+ status='New', owner_id=111, cc_ids=[222], labels=['a', 'b'],
+ component_ids=[1], blocked_on_iids=[78902], blocking_iids=[78903],
+ merged_into=78904, summary='Sum')
+ delta = tracker_pb2.IssueDelta()
+
+ amendments, impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+
+ self.assertEqual('New', issue.status)
+ self.assertEqual(111, issue.owner_id)
+ self.assertEqual([222], issue.cc_ids)
+ self.assertEqual(['a', 'b'], issue.labels)
+ self.assertEqual([1], issue.component_ids)
+ self.assertEqual([78902], issue.blocked_on_iids)
+ self.assertEqual([78903], issue.blocking_iids)
+ self.assertEqual(78904, issue.merged_into)
+ self.assertEqual('Sum', issue.summary)
+
+ self.assertEqual(0, len(amendments))
+ self.assertEqual(0, len(impacted_iids))
+
+ def testApplyIssueDelta_BuiltInFields(self):
+ """A delta can change built-in fields."""
+ ref_issue_70 = fake.MakeTestIssue(
+ 789, 70, 'Something that must be done before', 'New', 111)
+ self.services.issue.TestAddIssue(ref_issue_70)
+ ref_issue_71 = fake.MakeTestIssue(
+ 789, 71, 'Something that can only be done after', 'New', 111)
+ self.services.issue.TestAddIssue(ref_issue_71)
+ ref_issue_72 = fake.MakeTestIssue(
+ 789, 72, 'Something that seems the same', 'New', 111)
+ self.services.issue.TestAddIssue(ref_issue_72)
+ ref_issue_73 = fake.MakeTestIssue(
+ 789, 73, 'Something that used to seem the same', 'New', 111)
+ self.services.issue.TestAddIssue(ref_issue_73)
+ issue = tracker_pb2.Issue(
+ status='New', owner_id=111, cc_ids=[222], labels=['a', 'b'],
+ component_ids=[1], blocked_on_iids=[78902], blocking_iids=[78903],
+ merged_into=ref_issue_73.issue_id, summary='Sum')
+ delta = tracker_pb2.IssueDelta(
+ status='Duplicate', owner_id=999, cc_ids_add=[333, 444],
+ comp_ids_add=[2], labels_add=['c', 'd'],
+ blocked_on_add=[ref_issue_70.issue_id],
+ blocking_add=[ref_issue_71.issue_id],
+ merged_into=ref_issue_72.issue_id, summary='New summary')
+
+ actual_amendments, actual_impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+
+ self.assertEqual('Duplicate', issue.status)
+ self.assertEqual(999, issue.owner_id)
+ self.assertEqual([222, 333, 444], issue.cc_ids)
+ self.assertEqual([1, 2], issue.component_ids)
+ self.assertEqual(['a', 'b', 'c', 'd'], issue.labels)
+ self.assertEqual([78902, ref_issue_70.issue_id], issue.blocked_on_iids)
+ self.assertEqual([78903, ref_issue_71.issue_id], issue.blocking_iids)
+ self.assertEqual(ref_issue_72.issue_id, issue.merged_into)
+ self.assertEqual('New summary', issue.summary)
+
+ self.assertEqual(
+ [tracker_bizobj.MakeStatusAmendment('Duplicate', 'New'),
+ tracker_bizobj.MakeOwnerAmendment(999, 111),
+ tracker_bizobj.MakeCcAmendment([333, 444], []),
+ tracker_bizobj.MakeComponentsAmendment([2], [], self.config),
+ tracker_bizobj.MakeLabelsAmendment(['c', 'd'], []),
+ tracker_bizobj.MakeBlockedOnAmendment([(None, 70)], []),
+ tracker_bizobj.MakeBlockingAmendment([(None, 71)], []),
+ tracker_bizobj.MakeMergedIntoAmendment([(None, 72)], [(None, 73)]),
+ tracker_bizobj.MakeSummaryAmendment('New summary', 'Sum'),
+ ],
+ actual_amendments)
+ self.assertEqual(
+ set([ref_issue_70.issue_id, ref_issue_71.issue_id,
+ ref_issue_72.issue_id, ref_issue_73.issue_id]),
+ actual_impacted_iids)
+
+ def testApplyIssueDelta_ReferrencedIssueNotFound(self):
+ """This part of the code copes with missing issues."""
+ issue = tracker_pb2.Issue(
+ status='New', owner_id=111, cc_ids=[222], labels=['a', 'b'],
+ component_ids=[1], blocked_on_iids=[78902], blocking_iids=[78903],
+ merged_into=78904, summary='Sum')
+ delta = tracker_pb2.IssueDelta(
+ blocked_on_add=[78905], blocked_on_remove=[78902],
+ blocking_add=[78906], blocking_remove=[78903],
+ merged_into=78907)
+
+ actual_amendments, actual_impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+
+ self.assertEqual([78905], issue.blocked_on_iids)
+ self.assertEqual([78906], issue.blocking_iids)
+ self.assertEqual(78907, issue.merged_into)
+
+ self.assertEqual(
+ [tracker_bizobj.MakeBlockedOnAmendment([], []),
+ tracker_bizobj.MakeBlockingAmendment([], []),
+ tracker_bizobj.MakeMergedIntoAmendment([], []),
+ ],
+ actual_amendments)
+ self.assertEqual(
+ set([78902, 78903, 78905, 78906]),
+ actual_impacted_iids)
+
+ def testApplyIssueDelta_CustomPhaseFields(self):
+ """A delta can add, remove, or clear custom phase fields."""
+ fd_a = tracker_pb2.FieldDef(
+ field_id=1, project_id=789, field_name='a',
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ is_multivalued=True, is_phase_field=True)
+ fd_b = tracker_pb2.FieldDef(
+ field_id=2, project_id=789, field_name='b',
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ is_phase_field=True)
+ fd_c = tracker_pb2.FieldDef(
+ field_id=3, project_id=789, field_name='c',
+ field_type=tracker_pb2.FieldTypes.INT_TYPE, is_phase_field=True)
+ self.config.field_defs = [fd_a, fd_b, fd_c]
+ fv_a1_p1 = tracker_pb2.FieldValue(
+ field_id=1, int_value=1, phase_id=1) # fv
+ fv_a2_p1 = tracker_pb2.FieldValue(
+ field_id=1, int_value=2, phase_id=1) # add
+ fv_a3_p1 = tracker_pb2.FieldValue(
+ field_id=1, int_value=3, phase_id=1) # add
+ fv_b1_p1 = tracker_pb2.FieldValue(
+ field_id=2, int_value=1, phase_id=1) # add
+ fv_c2_p1 = tracker_pb2.FieldValue(
+ field_id=3, int_value=2, phase_id=1) # clear
+
+ fv_a2_p2 = tracker_pb2.FieldValue(
+ field_id=1, int_value=2, phase_id=2) # add
+ fv_b1_p2 = tracker_pb2.FieldValue(
+ field_id=2, int_value=1, phase_id=2) # fv remove
+ fv_c1_p2 = tracker_pb2.FieldValue(
+ field_id=3, int_value=1, phase_id=2) # clear
+
+ issue = tracker_pb2.Issue(
+ status='New', owner_id=111, summary='Sum',
+ field_values=[fv_a1_p1, fv_c2_p1, fv_b1_p2, fv_c1_p2])
+ issue.phases = [
+ tracker_pb2.Phase(phase_id=1, name='Phase-1'),
+ tracker_pb2.Phase(phase_id=2, name='Phase-2')]
+
+ delta = tracker_pb2.IssueDelta(
+ field_vals_add=[fv_a2_p1, fv_a3_p1, fv_b1_p1, fv_a2_p2],
+ field_vals_remove=[fv_b1_p2], fields_clear=[3])
+
+ actual_amendments, actual_impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+ self.assertEqual(
+ [tracker_bizobj.MakeFieldAmendment(
+ 1, self.config, ['2', '3'], [], phase_name='Phase-1'),
+ tracker_bizobj.MakeFieldAmendment(
+ 1, self.config, ['2'], [], phase_name='Phase-2'),
+ tracker_bizobj.MakeFieldAmendment(
+ 2, self.config, ['1'], [], phase_name='Phase-1'),
+ tracker_bizobj.MakeFieldAmendment(
+ 2, self.config, [], ['1'], phase_name='Phase-2'),
+ tracker_bizobj.MakeFieldClearedAmendment(3, self.config)],
+ actual_amendments)
+ self.assertEqual(set(), actual_impacted_iids)
+
+ def testApplyIssueDelta_CustomFields(self):
+ """A delta can add, remove, or clear custom fields."""
+ fd_a = tracker_pb2.FieldDef(
+ field_id=1, project_id=789, field_name='a',
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ is_multivalued=True)
+ fd_b = tracker_pb2.FieldDef(
+ field_id=2, project_id=789, field_name='b',
+ field_type=tracker_pb2.FieldTypes.INT_TYPE)
+ fd_c = tracker_pb2.FieldDef(
+ field_id=3, project_id=789, field_name='c',
+ field_type=tracker_pb2.FieldTypes.INT_TYPE)
+ fd_d = tracker_pb2.FieldDef(
+ field_id=4, project_id=789, field_name='d',
+ field_type=tracker_pb2.FieldTypes.ENUM_TYPE)
+ self.config.field_defs = [fd_a, fd_b, fd_c, fd_d]
+ fv_a1 = tracker_pb2.FieldValue(field_id=1, int_value=1)
+ fv_a2 = tracker_pb2.FieldValue(field_id=1, int_value=2)
+ fv_b1 = tracker_pb2.FieldValue(field_id=2, int_value=1)
+ fv_c1 = tracker_pb2.FieldValue(field_id=3, int_value=1)
+ issue = tracker_pb2.Issue(
+ status='New', owner_id=111, labels=['d-val', 'Hot'], summary='Sum',
+ field_values=[fv_a1, fv_b1, fv_c1])
+ delta = tracker_pb2.IssueDelta(
+ field_vals_add=[fv_a2], field_vals_remove=[fv_b1], fields_clear=[3, 4])
+
+ actual_amendments, actual_impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+
+ self.assertEqual([fv_a1, fv_a2], issue.field_values)
+ self.assertEqual(['Hot'], issue.labels)
+
+ self.assertEqual(
+ [tracker_bizobj.MakeFieldAmendment(1, self.config, ['2'], []),
+ tracker_bizobj.MakeFieldAmendment(2, self.config, [], ['1']),
+ tracker_bizobj.MakeFieldClearedAmendment(3, self.config),
+ tracker_bizobj.MakeFieldClearedAmendment(4, self.config),
+ ],
+ actual_amendments)
+ self.assertEqual(set(), actual_impacted_iids)
+
+ def testApplyIssueDelta_ExternalRefs(self):
+ """Only applies valid issue refs from a delta."""
+ issue = tracker_pb2.Issue(
+ status='New', owner_id=111, cc_ids=[222], labels=['a', 'b'],
+ component_ids=[1], blocked_on_iids=[78902], blocking_iids=[78903],
+ merged_into=78904, summary='Sum',
+ dangling_blocked_on_refs=[
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/345'),
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/111')],
+ dangling_blocking_refs=[
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/789'),
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/222')])
+ delta = tracker_pb2.IssueDelta(
+ # Add one valid, one invalid, and another valid.
+ ext_blocked_on_add=['b/123', 'b123', 'b/234'],
+ # Remove one valid, one invalid, and one that does not exist.
+ ext_blocked_on_remove=['b/345', 'b', 'b/456'],
+ # Add one valid, one invalid, and another valid.
+ ext_blocking_add=['b/567', 'b//123', 'b/678'],
+ # Remove one valid, one invalid, and one that does not exist.
+ ext_blocking_remove=['b/789', 'b/123/123', 'b/890'])
+
+ amendments, impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+
+ # Test amendments.
+ self.assertEqual(2, len(amendments))
+ self.assertEqual(tracker_pb2.FieldID.BLOCKEDON, amendments[0].field)
+ self.assertEqual('-b/345 b/123 b/234', amendments[0].newvalue)
+ self.assertEqual(tracker_pb2.FieldID.BLOCKING, amendments[1].field)
+ self.assertEqual('-b/789 b/567 b/678', amendments[1].newvalue)
+
+ self.assertEqual(0, len(impacted_iids))
+
+ # Issue refs are applied correctly and alphabetized.
+ self.assertEqual([
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/111'),
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/123'),
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/234'),
+ ], issue.dangling_blocked_on_refs)
+ self.assertEqual([
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/222'),
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/567'),
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/678'),
+ ], issue.dangling_blocking_refs)
+
+ def testApplyIssueDelta_AddAndRemoveExtRef(self):
+ """Only applies valid issue refs from a delta."""
+ issue = tracker_pb2.Issue(
+ status='New',
+ summary='Sum',
+ dangling_blocked_on_refs=[
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/111')
+ ],
+ dangling_blocking_refs=[
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/222')
+ ])
+ delta = tracker_pb2.IssueDelta(
+ ext_blocked_on_add=['b/123'],
+ ext_blocked_on_remove=['b/123'],
+ ext_blocking_add=['b/456'],
+ ext_blocking_remove=['b/456'])
+
+ amendments, impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+
+ # Nothing changed.
+ self.assertEqual(0, len(amendments))
+ self.assertEqual(0, len(impacted_iids))
+
+ self.assertEqual(
+ [tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/111')],
+ issue.dangling_blocked_on_refs)
+ self.assertEqual(
+ [tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/222')],
+ issue.dangling_blocking_refs)
+
+ def testApplyIssueDelta_OnlyInvalidExternalRefs(self):
+ """Only applies valid issue refs from a delta."""
+ issue = tracker_pb2.Issue(
+ status='New',
+ summary='Sum',
+ dangling_blocked_on_refs=[
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/111')
+ ],
+ dangling_blocking_refs=[
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/222')
+ ])
+ delta = tracker_pb2.IssueDelta(
+ # Add one invalid and one that already exists.
+ ext_blocked_on_add=['b123', 'b/111'],
+ # Remove one invalid, and one that does not exist.
+ ext_blocked_on_remove=['b', 'b/456'],
+ # Add one invalid and one that already exists.
+ ext_blocking_add=['b//123', 'b/222'],
+ # Remove one invalid, and one that does not exist.
+ ext_blocking_remove=['b/123/123', 'b/890'])
+
+ amendments, impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+
+ # Nothing changed.
+ self.assertEqual(0, len(amendments))
+ self.assertEqual(0, len(impacted_iids))
+
+ self.assertEqual(
+ [tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/111')],
+ issue.dangling_blocked_on_refs)
+ self.assertEqual(
+ [tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/222')],
+ issue.dangling_blocking_refs)
+
+ def testApplyIssueDelta_MergedIntoExternal(self):
+ """ApplyIssueDelta applies valid mergedinto refs."""
+ issue = tracker_pb2.Issue(status='New', owner_id=111)
+ delta = tracker_pb2.IssueDelta(merged_into_external='b/5678')
+ amendments, impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+
+ # Test amendments.
+ self.assertEqual(1, len(amendments))
+ self.assertEqual(tracker_pb2.FieldID.MERGEDINTO, amendments[0].field)
+ self.assertEqual('b/5678', amendments[0].newvalue)
+
+ self.assertEqual(0, len(impacted_iids))
+
+ # Issue refs are applied correctly and alphabetized.
+ self.assertEqual('b/5678', issue.merged_into_external)
+
+ def testApplyIssueDelta_MergedIntoExternalInvalid(self):
+ """ApplyIssueDelta does not accept invalid mergedinto refs."""
+ issue = tracker_pb2.Issue(status='New', owner_id=111)
+ delta = tracker_pb2.IssueDelta(merged_into_external='a/5678')
+ amendments, impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+
+ # No change.
+ self.assertEqual(0, len(amendments))
+ self.assertEqual(0, len(impacted_iids))
+ self.assertEqual(None, issue.merged_into_external)
+
+ def testApplyIssueDelta_MergedIntoFromInternalToExternal(self):
+ """ApplyIssueDelta updates from an internal to an external ref."""
+ self.services.issue.TestAddIssue(fake.MakeTestIssue(1, 2, 'Summary',
+ 'New', 111, issue_id=6789))
+ issue = tracker_pb2.Issue(status='New', owner_id=111, merged_into=6789)
+ delta = tracker_pb2.IssueDelta(merged_into_external='b/5678')
+ amendments, impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+
+ # Test amendments.
+ self.assertEqual(1, len(amendments))
+ self.assertEqual(tracker_pb2.FieldID.MERGEDINTO, amendments[0].field)
+ self.assertEqual('-2 b/5678', amendments[0].newvalue)
+ self.assertEqual(set([6789]), impacted_iids)
+ self.assertEqual(0, issue.merged_into)
+ self.assertEqual('b/5678', issue.merged_into_external)
+
+ def testApplyIssueDelta_MergedIntoFromExternalToInternal(self):
+ """ApplyIssueDelta updates from an external to an internalref."""
+ self.services.issue.TestAddIssue(fake.MakeTestIssue(1, 2, 'Summary',
+ 'New', 111, issue_id=6789))
+ issue = tracker_pb2.Issue(status='New', owner_id=111,
+ merged_into_external='b/5678')
+ delta = tracker_pb2.IssueDelta(merged_into=6789)
+ amendments, impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+
+ # Test amendments.
+ self.assertEqual(1, len(amendments))
+ self.assertEqual(tracker_pb2.FieldID.MERGEDINTO, amendments[0].field)
+ self.assertEqual('-b/5678 2', amendments[0].newvalue)
+ self.assertEqual(set([6789]), impacted_iids)
+ self.assertEqual(6789, issue.merged_into)
+ self.assertEqual(None, issue.merged_into_external)
+
+ def testApplyIssueDelta_MergedIntoFromExternalToExternal(self):
+ """ApplyIssueDelta updates from an external to another external ref."""
+ issue = tracker_pb2.Issue(
+ status='New', owner_id=111, merged_into_external='b/1')
+ delta = tracker_pb2.IssueDelta(merged_into_external='b/5678')
+ amendments, impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+
+ # Test amendments.
+ self.assertEqual(1, len(amendments))
+ self.assertEqual(tracker_pb2.FieldID.MERGEDINTO, amendments[0].field)
+ self.assertEqual('-b/1 b/5678', amendments[0].newvalue)
+ self.assertEqual(set(), impacted_iids)
+ self.assertEqual(0, issue.merged_into)
+ self.assertEqual('b/5678', issue.merged_into_external)
+
+ def testApplyIssueDelta_NoMergedIntoInternalAndExternal(self):
+ """ApplyIssueDelta does not allow updating the internal and external
+ merged_into fields at the same time.
+ """
+ issue = tracker_pb2.Issue(status='New', owner_id=111, merged_into=321)
+ delta = tracker_pb2.IssueDelta(merged_into=543,
+ merged_into_external='b/5678')
+ with self.assertRaises(ValueError):
+ tracker_bizobj.ApplyIssueDelta(self.cnxn, self.services.issue, issue,
+ delta, self.config)
+
+ def testApplyIssueDelta_RemoveExistingMergedInto(self):
+ self.services.issue.TestAddIssue(
+ fake.MakeTestIssue(1, 2, 'Summary', 'New', 111, issue_id=6789))
+ issue = tracker_pb2.Issue(status='New', owner_id=111, merged_into=6789)
+ delta = tracker_pb2.IssueDelta(merged_into=0)
+ amendments, impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+ self.assertEqual(impacted_iids, {6789})
+ self.assertEqual(1, len(amendments))
+ self.assertEqual(
+ amendments[0],
+ tracker_bizobj.MakeMergedIntoAmendment(
+ [], [(issue.project_name, 2)],
+ default_project_name=issue.project_name))
+ self.assertEqual(issue.merged_into, 0)
+
+ def testApplyIssueDelta_RemoveExternalMergedInto(self):
+ issue = tracker_pb2.Issue(
+ status='New', owner_id=111, merged_into_external='b/123')
+ delta = tracker_pb2.IssueDelta(merged_into_external='')
+ amendments, impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+ self.assertEqual(impacted_iids, set())
+ self.assertEqual(1, len(amendments))
+ self.assertEqual(
+ amendments[0],
+ tracker_bizobj.MakeMergedIntoAmendment(
+ [], [tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/123')]))
+ self.assertEqual(issue.merged_into_external, '')
+
+ def testApplyIssueDelta_RemoveMergedIntoNoop(self):
+ issue = tracker_pb2.Issue(
+ status='New', owner_id=111, merged_into_external='b/123')
+ delta = tracker_pb2.IssueDelta(merged_into=0)
+ amendments, impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+ self.assertEqual(impacted_iids, set())
+ self.assertEqual(0, len(amendments))
+ # A noop request to remove merged_into, should not affect the existing
+ # external value.
+ self.assertIsNone(issue.merged_into)
+ self.assertEqual(issue.merged_into_external, 'b/123')
+
+ def testApplyIssueDelta_RemoveExternalMergedIntoNoop(self):
+ self.services.issue.TestAddIssue(
+ fake.MakeTestIssue(1, 2, 'Summary', 'New', 111, issue_id=6789))
+ issue = tracker_pb2.Issue(status='New', owner_id=111, merged_into=6789)
+ delta = tracker_pb2.IssueDelta(merged_into_external='')
+ amendments, impacted_iids = tracker_bizobj.ApplyIssueDelta(
+ self.cnxn, self.services.issue, issue, delta, self.config)
+ self.assertEqual(impacted_iids, set())
+ self.assertEqual(len(amendments), 0)
+ # A noop request to remove merged_into_external, should not affect the
+ # existing internal merged_into value.
+ self.assertIsNone(issue.merged_into_external)
+ self.assertEqual(issue.merged_into, 6789)
+
+ def testApplyIssueBlockRelationChanges(self):
+ """We can apply blocking and blocked_on relation changes to an issue."""
+
+ blocked_on = fake.MakeTestIssue(
+ 789, 2, 'Something that must be done before', 'New', 111,
+ project_name='proj')
+ self.services.issue.TestAddIssue(blocked_on)
+ blocking = fake.MakeTestIssue(
+ 789, 3, 'Something that must be done after', 'New', 111,
+ project_name='proj')
+ self.services.issue.TestAddIssue(blocking)
+
+ issue = tracker_pb2.Issue(
+ project_name='chicken',
+ blocked_on_iids=[blocked_on.issue_id, 78904],
+ blocking_iids=[blocking.issue_id, 78905])
+ blocked_on_add = fake.MakeTestIssue(
+ 789, 6, 'Something that must be done before', 'New', 111,
+ project_name='chicken')
+ self.services.issue.TestAddIssue(blocked_on_add)
+ blocking_add = fake.MakeTestIssue(
+ 789, 7, 'Something that must be done after', 'New', 111,
+ project_name='chicken')
+ self.services.issue.TestAddIssue(blocking_add)
+
+ (actual_amendments, actual_impacted_iids
+ ) = tracker_bizobj.ApplyIssueBlockRelationChanges(
+ self.cnxn,
+ issue,
+ # 78904 ref already exists can't be added, shuold ignore.
+ # 78404 ref does not exist, can't be removed, should ignore.
+ # blocked_on is ignored in the add list, but honored in the remove.
+ [blocked_on_add.issue_id, 78904, blocked_on.issue_id],
+ [78404, blocked_on.issue_id],
+ # 78905 ref already exists, can't be added, should ignore.
+ # 79404 ref does not exist in issue, can't be removed, should ignore.
+ # blocking_add is ignored in the remove list, but honored in the add.
+ [blocking_add.issue_id, 78905],
+ [79404, blocking.issue_id, blocking_add.issue_id],
+ self.services.issue)
+
+ expected_amendments = [
+ tracker_bizobj.MakeBlockedOnAmendment(
+ [('chicken', blocked_on_add.local_id)],
+ [('proj', blocked_on.local_id)],
+ default_project_name=issue.project_name),
+ tracker_bizobj.MakeBlockingAmendment(
+ [('chicken', blocking_add.local_id)], [('proj', blocking.local_id)],
+ default_project_name=issue.project_name)
+ ]
+ self.assertEqual(actual_amendments, expected_amendments)
+ self.assertItemsEqual(
+ actual_impacted_iids, [
+ blocked_on_add.issue_id, blocking_add.issue_id, blocked_on.issue_id,
+ blocking.issue_id
+ ])
+ self.assertEqual(issue.blocked_on_iids, [78904, blocked_on_add.issue_id])
+ self.assertEqual(issue.blocking_iids, [78905, blocking_add.issue_id])
+
+ def testApplyIssueBlockRelationChanges_Empty(self):
+ """We can handle empty blocking and blocked_on relation changes."""
+ issue = tracker_pb2.Issue(blocked_on_iids=[78901], blocking_iids=[78902])
+ (actual_amendments,
+ actual_impacted_iids) = tracker_bizobj.ApplyIssueBlockRelationChanges(
+ self.cnxn, issue, [], [], [], [], self.services.issue)
+
+ self.assertEqual(actual_amendments, [])
+ self.assertEqual(actual_impacted_iids, set())
+ self.assertEqual(issue.blocked_on_iids, [78901])
+ self.assertEqual(issue.blocking_iids, [78902])
+
+ def testMakeAmendment(self):
+ amendment = tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.STATUS, 'new', [111], [222])
+ self.assertEqual(tracker_pb2.FieldID.STATUS, amendment.field)
+ self.assertEqual('new', amendment.newvalue)
+ self.assertEqual([111], amendment.added_user_ids)
+ self.assertEqual([222], amendment.removed_user_ids)
+
+ def testPlusMinusString(self):
+ self.assertEqual('', tracker_bizobj._PlusMinusString([], []))
+ self.assertEqual('-a -b c d',
+ tracker_bizobj._PlusMinusString(['c', 'd'], ['a', 'b']))
+
+ def testPlusMinusAmendment(self):
+ amendment = tracker_bizobj._PlusMinusAmendment(
+ tracker_pb2.FieldID.STATUS, ['add1', 'add2'], ['remove1'])
+ self.assertEqual(tracker_pb2.FieldID.STATUS, amendment.field)
+ self.assertEqual('-remove1 add1 add2', amendment.newvalue)
+
+ def testPlusMinusRefsAmendment(self):
+ ref1 = (None, 1)
+ ref2 = ('other-proj', 2)
+ amendment = tracker_bizobj._PlusMinusRefsAmendment(
+ tracker_pb2.FieldID.STATUS, [ref1], [ref2])
+ self.assertEqual(tracker_pb2.FieldID.STATUS, amendment.field)
+ self.assertEqual('-other-proj:2 1', amendment.newvalue)
+
+ def testMakeSummaryAmendment(self):
+ amendment = tracker_bizobj.MakeSummaryAmendment('', None)
+ self.assertEqual(tracker_pb2.FieldID.SUMMARY, amendment.field)
+ self.assertEqual('', amendment.newvalue)
+ self.assertEqual(None, amendment.oldvalue)
+
+ amendment = tracker_bizobj.MakeSummaryAmendment('new summary', '')
+ self.assertEqual(tracker_pb2.FieldID.SUMMARY, amendment.field)
+ self.assertEqual('new summary', amendment.newvalue)
+ self.assertEqual('', amendment.oldvalue)
+
+ def testMakeStatusAmendment(self):
+ amendment = tracker_bizobj.MakeStatusAmendment('', None)
+ self.assertEqual(tracker_pb2.FieldID.STATUS, amendment.field)
+ self.assertEqual('', amendment.newvalue)
+ self.assertEqual(None, amendment.oldvalue)
+
+ amendment = tracker_bizobj.MakeStatusAmendment('New', '')
+ self.assertEqual(tracker_pb2.FieldID.STATUS, amendment.field)
+ self.assertEqual('New', amendment.newvalue)
+ self.assertEqual('', amendment.oldvalue)
+
+ def testMakeOwnerAmendment(self):
+ amendment = tracker_bizobj.MakeOwnerAmendment(111, 0)
+ self.assertEqual(tracker_pb2.FieldID.OWNER, amendment.field)
+ self.assertEqual('', amendment.newvalue)
+ self.assertEqual([111], amendment.added_user_ids)
+ self.assertEqual([0], amendment.removed_user_ids)
+
+ def testMakeCcAmendment(self):
+ amendment = tracker_bizobj.MakeCcAmendment([111], [222])
+ self.assertEqual(tracker_pb2.FieldID.CC, amendment.field)
+ self.assertEqual('', amendment.newvalue)
+ self.assertEqual([111], amendment.added_user_ids)
+ self.assertEqual([222], amendment.removed_user_ids)
+
+ def testMakeLabelsAmendment(self):
+ amendment = tracker_bizobj.MakeLabelsAmendment(['added1'], ['removed1'])
+ self.assertEqual(tracker_pb2.FieldID.LABELS, amendment.field)
+ self.assertEqual('-removed1 added1', amendment.newvalue)
+
+ def testDiffValueLists(self):
+ added, removed = tracker_bizobj.DiffValueLists([], [])
+ self.assertItemsEqual([], added)
+ self.assertItemsEqual([], removed)
+
+ added, removed = tracker_bizobj.DiffValueLists([], None)
+ self.assertItemsEqual([], added)
+ self.assertItemsEqual([], removed)
+
+ added, removed = tracker_bizobj.DiffValueLists([1, 2], [])
+ self.assertItemsEqual([1, 2], added)
+ self.assertItemsEqual([], removed)
+
+ added, removed = tracker_bizobj.DiffValueLists([], [8, 9])
+ self.assertItemsEqual([], added)
+ self.assertItemsEqual([8, 9], removed)
+
+ added, removed = tracker_bizobj.DiffValueLists([1, 2], [8, 9])
+ self.assertItemsEqual([1, 2], added)
+ self.assertItemsEqual([8, 9], removed)
+
+ added, removed = tracker_bizobj.DiffValueLists([1, 2, 5, 6], [5, 6, 8, 9])
+ self.assertItemsEqual([1, 2], added)
+ self.assertItemsEqual([8, 9], removed)
+
+ added, removed = tracker_bizobj.DiffValueLists([5, 6], [5, 6, 8, 9])
+ self.assertItemsEqual([], added)
+ self.assertItemsEqual([8, 9], removed)
+
+ added, removed = tracker_bizobj.DiffValueLists([1, 2, 5, 6], [5, 6])
+ self.assertItemsEqual([1, 2], added)
+ self.assertItemsEqual([], removed)
+
+ added, removed = tracker_bizobj.DiffValueLists(
+ [1, 2, 2, 5, 6], [5, 6, 8, 9])
+ self.assertItemsEqual([1, 2, 2], added)
+ self.assertItemsEqual([8, 9], removed)
+
+ added, removed = tracker_bizobj.DiffValueLists(
+ [1, 2, 5, 6], [5, 6, 8, 8, 9])
+ self.assertItemsEqual([1, 2], added)
+ self.assertItemsEqual([8, 8, 9], removed)
+
+ def testMakeFieldAmendment_NoSuchFieldDef(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ with self.assertRaises(ValueError):
+ tracker_bizobj.MakeFieldAmendment(1, config, ['Large'], ['Small'])
+
+ def testMakeFieldAmendment_MultiValued(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ fd = tracker_pb2.FieldDef(
+ field_id=1, field_name='Days', is_multivalued=True)
+ config.field_defs.append(fd)
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.CUSTOM, '-Mon Tue Wed', [], [], 'Days'),
+ tracker_bizobj.MakeFieldAmendment(1, config, ['Tue', 'Wed'], ['Mon']))
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.CUSTOM, '-Mon', [], [], 'Days'),
+ tracker_bizobj.MakeFieldAmendment(1, config, [], ['Mon']))
+
+ def testMakeFieldAmendment_MultiValuedUser(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ fd = tracker_pb2.FieldDef(
+ field_id=1, field_name='Friends', is_multivalued=True,
+ field_type=tracker_pb2.FieldTypes.USER_TYPE)
+ config.field_defs.append(fd)
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.CUSTOM, '', [111], [222], 'Friends'),
+ tracker_bizobj.MakeFieldAmendment(1, config, [111], [222]))
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.CUSTOM, '', [], [222], 'Friends'),
+ tracker_bizobj.MakeFieldAmendment(1, config, [], [222]))
+
+ def testMakeFieldAmendment_SingleValued(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ fd = tracker_pb2.FieldDef(field_id=1, field_name='Size')
+ config.field_defs.append(fd)
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.CUSTOM, 'Large', [], [], 'Size'),
+ tracker_bizobj.MakeFieldAmendment(1, config, ['Large'], ['Small']))
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.CUSTOM, '----', [], [], 'Size'),
+ tracker_bizobj.MakeFieldAmendment(1, config, [], ['Small']))
+
+ def testMakeFieldAmendment_SingleValuedUser(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ fd = tracker_pb2.FieldDef(
+ field_id=1, field_name='Friend',
+ field_type=tracker_pb2.FieldTypes.USER_TYPE)
+ config.field_defs.append(fd)
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.CUSTOM, '', [111], [], 'Friend'),
+ tracker_bizobj.MakeFieldAmendment(1, config, [111], [222]))
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.CUSTOM, '', [], [], 'Friend'),
+ tracker_bizobj.MakeFieldAmendment(1, config, [], [222]))
+
+ def testMakeFieldAmendment_PhaseField(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ fd = tracker_pb2.FieldDef(
+ field_id=1, field_name='Friend',
+ field_type=tracker_pb2.FieldTypes.USER_TYPE, is_phase_field=True)
+ config.field_defs.append(fd)
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.CUSTOM, '', [111], [], 'PhaseName-Friend'),
+ tracker_bizobj.MakeFieldAmendment(
+ 1, config, [111], [222], phase_name='PhaseName'))
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.CUSTOM, '', [], [], 'PhaseName-3-Friend'),
+ tracker_bizobj.MakeFieldAmendment(
+ 1, config, [], [222], phase_name='PhaseName-3'))
+
+ def testMakeFieldClearedAmendment_FieldNotFound(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ with self.assertRaises(ValueError):
+ tracker_bizobj.MakeFieldClearedAmendment(1, config)
+
+ def testMakeFieldClearedAmendment_Normal(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ fd = tracker_pb2.FieldDef(field_id=1, field_name='Rabbit')
+ config.field_defs.append(fd)
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.CUSTOM, '----', [], [], 'Rabbit'),
+ tracker_bizobj.MakeFieldClearedAmendment(1, config))
+
+ def testMakeApprovalStructureAmendment(self):
+ actual_amendment = tracker_bizobj.MakeApprovalStructureAmendment(
+ ['Chicken1', 'Chicken', 'Llama'], ['Cow', 'Chicken2', 'Llama'])
+ amendment = tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.CUSTOM, '-Cow -Chicken2 Chicken1 Chicken',
+ [], [], 'Approvals')
+ self.assertEqual(amendment, actual_amendment)
+
+ def testMakeApprovalStatusAmendment(self):
+ actual_amendment = tracker_bizobj.MakeApprovalStatusAmendment(
+ tracker_pb2.ApprovalStatus.APPROVED)
+ amendment = tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.CUSTOM, newvalue='approved',
+ custom_field_name='Status')
+ self.assertEqual(amendment, actual_amendment)
+
+ def testMakeApprovalApproversAmendment(self):
+ actual_amendment = tracker_bizobj.MakeApprovalApproversAmendment(
+ [222], [333])
+ amendment = tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.CUSTOM, newvalue='', added_user_ids=[222],
+ removed_user_ids=[333], custom_field_name='Approvers')
+ self.assertEqual(actual_amendment, amendment)
+
+ def testMakeComponentsAmendment_NoChange(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI'),
+ tracker_pb2.ComponentDef(component_id=2, path='DB')]
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.COMPONENTS, '', [], []),
+ tracker_bizobj.MakeComponentsAmendment([], [], config))
+
+ def testMakeComponentsAmendment_NotFound(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI'),
+ tracker_pb2.ComponentDef(component_id=2, path='DB')]
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.COMPONENTS, '', [], []),
+ tracker_bizobj.MakeComponentsAmendment([99], [999], config))
+
+ def testMakeComponentsAmendment_Normal(self):
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI'),
+ tracker_pb2.ComponentDef(component_id=2, path='DB')]
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.COMPONENTS, '-UI DB', [], []),
+ tracker_bizobj.MakeComponentsAmendment([2], [1], config))
+
+ def testMakeBlockedOnAmendment(self):
+ ref1 = (None, 1)
+ ref2 = ('other-proj', 2)
+ amendment = tracker_bizobj.MakeBlockedOnAmendment([ref1], [ref2])
+ self.assertEqual(tracker_pb2.FieldID.BLOCKEDON, amendment.field)
+ self.assertEqual('-other-proj:2 1', amendment.newvalue)
+
+ amendment = tracker_bizobj.MakeBlockedOnAmendment([ref2], [ref1])
+ self.assertEqual(tracker_pb2.FieldID.BLOCKEDON, amendment.field)
+ self.assertEqual('-1 other-proj:2', amendment.newvalue)
+
+ def testMakeBlockingAmendment(self):
+ ref1 = (None, 1)
+ ref2 = ('other-proj', 2)
+ amendment = tracker_bizobj.MakeBlockingAmendment([ref1], [ref2])
+ self.assertEqual(tracker_pb2.FieldID.BLOCKING, amendment.field)
+ self.assertEqual('-other-proj:2 1', amendment.newvalue)
+
+ def testMakeMergedIntoAmendment(self):
+ ref1 = (None, 1)
+ ref2 = ('other-proj', 2)
+ ref3 = ('chicken-proj', 3)
+ amendment = tracker_bizobj.MakeMergedIntoAmendment(
+ [ref1, None], [ref2, ref3])
+ self.assertEqual(tracker_pb2.FieldID.MERGEDINTO, amendment.field)
+ self.assertEqual('-other-proj:2 -chicken-proj:3 1', amendment.newvalue)
+
+ def testMakeProjectAmendment(self):
+ self.assertEqual(
+ tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.PROJECT, 'moonshot', [], []),
+ tracker_bizobj.MakeProjectAmendment('moonshot'))
+
+ def testAmendmentString(self):
+ users_by_id = {
+ 111: framework_views.StuffUserView(111, 'username@gmail.com', True),
+ framework_constants.DELETED_USER_ID: framework_views.StuffUserView(
+ framework_constants.DELETED_USER_ID, '', True),
+ }
+ summary_amendment = tracker_bizobj.MakeSummaryAmendment('new summary', None)
+ self.assertEqual(
+ 'new summary',
+ tracker_bizobj.AmendmentString(summary_amendment, users_by_id))
+
+ status_amendment = tracker_bizobj.MakeStatusAmendment('', None)
+ self.assertEqual(
+ '', tracker_bizobj.AmendmentString(status_amendment, users_by_id))
+ status_amendment = tracker_bizobj.MakeStatusAmendment('Assigned', 'New')
+ self.assertEqual(
+ 'Assigned',
+ tracker_bizobj.AmendmentString(status_amendment, users_by_id))
+
+ owner_amendment = tracker_bizobj.MakeOwnerAmendment(0, 0)
+ self.assertEqual(
+ '----', tracker_bizobj.AmendmentString(owner_amendment, users_by_id))
+ owner_amendment = tracker_bizobj.MakeOwnerAmendment(111, 0)
+ self.assertEqual(
+ 'usern...@gmail.com',
+ tracker_bizobj.AmendmentString(owner_amendment, users_by_id))
+
+ owner_amendment_deleted = tracker_bizobj.MakeOwnerAmendment(1, 0)
+ self.assertEqual(
+ framework_constants.DELETED_USER_NAME,
+ tracker_bizobj.AmendmentString(owner_amendment_deleted, users_by_id))
+
+ def testAmendmentString_New(self):
+ """AmendmentString_New behaves equivalently to the old version."""
+ # TODO(crbug.com/monorail/7571): Delete this test.
+ users_by_id = {
+ 111:
+ framework_views.StuffUserView(111, 'username@gmail.com', True),
+ framework_constants.DELETED_USER_ID:
+ framework_views.StuffUserView(
+ framework_constants.DELETED_USER_ID, '', True),
+ }
+ user_display_names = {
+ 111:
+ 'usern...@gmail.com',
+ framework_constants.DELETED_USER_ID:
+ framework_constants.DELETED_USER_NAME
+ }
+
+ summary_amendment = tracker_bizobj.MakeSummaryAmendment('new summary', None)
+ new_str_summary = tracker_bizobj.AmendmentString_New(
+ summary_amendment, user_display_names)
+ self.assertEqual(
+ tracker_bizobj.AmendmentString(summary_amendment, users_by_id),
+ new_str_summary)
+
+ status_amendment = tracker_bizobj.MakeStatusAmendment('', None)
+ new_str_status = tracker_bizobj.AmendmentString_New(
+ status_amendment, user_display_names)
+ self.assertEqual(
+ tracker_bizobj.AmendmentString(status_amendment, users_by_id),
+ new_str_status)
+
+ status_amendment_2 = tracker_bizobj.MakeStatusAmendment('Assigned', 'New')
+ new_str_status_2 = tracker_bizobj.AmendmentString_New(
+ status_amendment_2, user_display_names)
+ self.assertEqual(
+ tracker_bizobj.AmendmentString(status_amendment_2, users_by_id),
+ new_str_status_2)
+
+ owner_amendment = tracker_bizobj.MakeOwnerAmendment(0, 0)
+ new_str_owner = tracker_bizobj.AmendmentString_New(
+ owner_amendment, user_display_names)
+ self.assertEqual(
+ tracker_bizobj.AmendmentString(owner_amendment, users_by_id),
+ new_str_owner)
+
+ owner_amendment_2 = tracker_bizobj.MakeOwnerAmendment(111, 0)
+ new_str_owner_2 = tracker_bizobj.AmendmentString_New(
+ owner_amendment_2, user_display_names)
+ self.assertEqual(
+ tracker_bizobj.AmendmentString(owner_amendment_2, users_by_id),
+ new_str_owner_2)
+
+ owner_amendment_deleted = tracker_bizobj.MakeOwnerAmendment(1, 0)
+ new_str_owner_deleted = tracker_bizobj.AmendmentString_New(
+ owner_amendment_deleted, user_display_names)
+ self.assertEqual(
+ tracker_bizobj.AmendmentString(owner_amendment_deleted, users_by_id),
+ new_str_owner_deleted)
+
+
+ def testAmendmentLinks(self):
+ users_by_id = {
+ 111: framework_views.StuffUserView(111, 'foo@gmail.com', False),
+ 222: framework_views.StuffUserView(222, 'bar@gmail.com', False),
+ 333: framework_views.StuffUserView(333, 'baz@gmail.com', False),
+ framework_constants.DELETED_USER_ID: framework_views.StuffUserView(
+ framework_constants.DELETED_USER_ID, '', True),
+ }
+ # SUMMARY
+ summary_amendment = tracker_bizobj.MakeSummaryAmendment('new summary', None)
+ self.assertEqual(
+ [{'value': 'new summary', 'url': None}],
+ tracker_bizobj.AmendmentLinks(summary_amendment, users_by_id, 'proj'))
+
+ summary_amendment = tracker_bizobj.MakeSummaryAmendment(
+ 'new summary', 'NULL')
+ self.assertEqual(
+ [{'value': 'new summary', 'url': None}],
+ tracker_bizobj.AmendmentLinks(summary_amendment, users_by_id, 'proj'))
+
+ summary_amendment = tracker_bizobj.MakeSummaryAmendment(
+ 'new summary', 'old info')
+ self.assertEqual(
+ [{'value': 'new summary (was: old info)', 'url': None}],
+ tracker_bizobj.AmendmentLinks(summary_amendment, users_by_id, 'proj'))
+
+ # STATUS
+ status_amendment = tracker_bizobj.MakeStatusAmendment('New', None)
+ self.assertEqual(
+ [{'value': 'New', 'url': None}],
+ tracker_bizobj.AmendmentLinks(status_amendment, users_by_id, 'proj'))
+
+ status_amendment = tracker_bizobj.MakeStatusAmendment('New', 'NULL')
+ self.assertEqual(
+ [{'value': 'New', 'url': None}],
+ tracker_bizobj.AmendmentLinks(status_amendment, users_by_id, 'proj'))
+
+ status_amendment = tracker_bizobj.MakeStatusAmendment(
+ 'Assigned', 'New')
+ self.assertEqual(
+ [{'value': 'Assigned (was: New)', 'url': None}],
+ tracker_bizobj.AmendmentLinks(status_amendment, users_by_id, 'proj'))
+
+ # OWNER
+ owner_amendment = tracker_bizobj.MakeOwnerAmendment(0, 0)
+ self.assertEqual(
+ [{'value': '----', 'url': None}],
+ tracker_bizobj.AmendmentLinks(owner_amendment, users_by_id, 'proj'))
+ owner_amendment = tracker_bizobj.MakeOwnerAmendment(111, 0)
+ self.assertEqual(
+ [{'value': 'foo@gmail.com', 'url': None}],
+ tracker_bizobj.AmendmentLinks(owner_amendment, users_by_id, 'proj'))
+
+ # BLOCKEDON, BLOCKING, MERGEDINTO
+ blocking_amendment = tracker_bizobj.MakeBlockingAmendment(
+ [(None, 123), ('blah', 234)], [(None, 345), ('blah', 456)])
+ self.assertEqual([
+ {'value': '-345', 'url': '/p/proj/issues/detail?id=345'},
+ {'value': '-blah:456', 'url': '/p/blah/issues/detail?id=456'},
+ {'value': '123', 'url': '/p/proj/issues/detail?id=123'},
+ {'value': 'blah:234', 'url': '/p/blah/issues/detail?id=234'}],
+ tracker_bizobj.AmendmentLinks(blocking_amendment, users_by_id, 'proj'))
+
+ # newvalue catchall
+ label_amendment = tracker_bizobj.MakeLabelsAmendment(
+ ['My-Label', 'Your-Label'], ['Their-Label'])
+ self.assertEqual([
+ {'value': '-Their-Label', 'url': None},
+ {'value': 'My-Label', 'url': None},
+ {'value': 'Your-Label', 'url': None}],
+ tracker_bizobj.AmendmentLinks(label_amendment, users_by_id, 'proj'))
+
+ # CC, or CUSTOM with user type
+ cc_amendment = tracker_bizobj.MakeCcAmendment([222, 333], [111])
+ self.assertEqual([
+ {'value': '-foo@gmail.com', 'url': None},
+ {'value': 'bar@gmail.com', 'url': None},
+ {'value': 'baz@gmail.com', 'url': None}],
+ tracker_bizobj.AmendmentLinks(cc_amendment, users_by_id, 'proj'))
+ user_amendment = tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.CUSTOM, None, [222, 333], [111], 'ultracc')
+ self.assertEqual([
+ {'value': '-foo@gmail.com', 'url': None},
+ {'value': 'bar@gmail.com', 'url': None},
+ {'value': 'baz@gmail.com', 'url': None}],
+ tracker_bizobj.AmendmentLinks(user_amendment, users_by_id, 'proj'))
+
+ # deleted users
+ cc_amendment_deleted = tracker_bizobj.MakeCcAmendment(
+ [framework_constants.DELETED_USER_ID], [])
+ self.assertEqual(
+ [{'value': framework_constants.DELETED_USER_NAME, 'url': None}],
+ tracker_bizobj.AmendmentLinks(
+ cc_amendment_deleted, users_by_id, 'proj'))
+
+ def testGetAmendmentFieldName_Custom(self):
+ amendment = tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.CUSTOM, None, [222, 333], [111], 'Rabbit')
+ self.assertEqual('Rabbit', tracker_bizobj.GetAmendmentFieldName(amendment))
+
+ def testGetAmendmentFieldName_Builtin(self):
+ amendment = tracker_bizobj.MakeAmendment(
+ tracker_pb2.FieldID.SUMMARY, 'It broke', [], [])
+ self.assertEqual('Summary', tracker_bizobj.GetAmendmentFieldName(amendment))
+
+ def testMakeDanglingIssueRef(self):
+ di_ref = tracker_bizobj.MakeDanglingIssueRef('proj', 123)
+ self.assertEqual('proj', di_ref.project)
+ self.assertEqual(123, di_ref.issue_id)
+
+ def testFormatIssueURL_NoRef(self):
+ self.assertEqual('', tracker_bizobj.FormatIssueURL(None))
+
+ def testFormatIssueRef(self):
+ self.assertEqual('', tracker_bizobj.FormatIssueRef(None))
+
+ self.assertEqual(
+ 'p:1', tracker_bizobj.FormatIssueRef(('p', 1)))
+
+ self.assertEqual(
+ '1', tracker_bizobj.FormatIssueRef((None, 1)))
+
+ def testFormatIssueRef_External(self):
+ """Outputs shortlink as-is."""
+ ref = tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/1234')
+ self.assertEqual('b/1234', tracker_bizobj.FormatIssueRef(ref))
+
+ def testFormatIssueRef_ExternalInvalid(self):
+ """Does not validate external IDs."""
+ ref = tracker_pb2.DanglingIssueRef(ext_issue_identifier='invalid')
+ self.assertEqual('invalid', tracker_bizobj.FormatIssueRef(ref))
+
+ def testFormatIssueRef_Empty(self):
+ """Passes on empty values."""
+ ref = tracker_pb2.DanglingIssueRef(ext_issue_identifier='')
+ self.assertEqual('', tracker_bizobj.FormatIssueRef(ref))
+
+ def testParseIssueRef(self):
+ self.assertEqual(None, tracker_bizobj.ParseIssueRef(''))
+ self.assertEqual(None, tracker_bizobj.ParseIssueRef(' \t '))
+
+ ref_pn, ref_id = tracker_bizobj.ParseIssueRef('1')
+ self.assertEqual(None, ref_pn)
+ self.assertEqual(1, ref_id)
+
+ ref_pn, ref_id = tracker_bizobj.ParseIssueRef('-1')
+ self.assertEqual(None, ref_pn)
+ self.assertEqual(1, ref_id)
+
+ ref_pn, ref_id = tracker_bizobj.ParseIssueRef('p:2')
+ self.assertEqual('p', ref_pn)
+ self.assertEqual(2, ref_id)
+
+ ref_pn, ref_id = tracker_bizobj.ParseIssueRef('-p:2')
+ self.assertEqual('p', ref_pn)
+ self.assertEqual(2, ref_id)
+
+ def testSafeParseIssueRef(self):
+ self.assertEqual(None, tracker_bizobj._SafeParseIssueRef('-'))
+ self.assertEqual(None, tracker_bizobj._SafeParseIssueRef('test:'))
+ ref_pn, ref_id = tracker_bizobj.ParseIssueRef('p:2')
+ self.assertEqual('p', ref_pn)
+ self.assertEqual(2, ref_id)
+
+ def testMergeFields_NoChange(self):
+ fv1 = tracker_bizobj.MakeFieldValue(1, 42, None, None, None, None, False)
+ merged_fvs, fvs_added_dict, fvs_removed_dict = tracker_bizobj._MergeFields(
+ [fv1], [], [], [])
+ self.assertEqual([fv1], merged_fvs)
+ self.assertEqual({}, fvs_added_dict)
+ self.assertEqual({}, fvs_removed_dict)
+
+ def testMergeFields_SingleValued(self):
+ fd = tracker_pb2.FieldDef(field_id=1, field_name='foo')
+ fv1 = tracker_bizobj.MakeFieldValue(1, 42, None, None, None, None, False)
+ fv2 = tracker_bizobj.MakeFieldValue(1, 43, None, None, None, None, False)
+ fv3 = tracker_bizobj.MakeFieldValue(1, 44, None, None, None, None, False)
+
+ # Adding one replaces all values since the field is single-valued.
+ merged_fvs, fvs_added_dict, fvs_removed_dict = tracker_bizobj._MergeFields(
+ [fv1, fv2], [fv3], [], [fd])
+ self.assertEqual([fv3], merged_fvs)
+ self.assertEqual({fv3.field_id: [fv3]}, fvs_added_dict)
+ self.assertEqual({}, fvs_removed_dict)
+
+ # Removing one just removes it, does not reset.
+ merged_fvs, fvs_added_dict, fvs_removed_dict = tracker_bizobj._MergeFields(
+ [fv1, fv2], [], [fv2], [fd])
+ self.assertEqual([fv1], merged_fvs)
+ self.assertEqual({}, fvs_added_dict)
+ self.assertEqual({fv2.field_id: [fv2]}, fvs_removed_dict)
+
+ def testMergeFields_SingleValuedPhase(self):
+ fd = tracker_pb2.FieldDef(
+ field_id=1, field_name='phase-foo', is_phase_field=True)
+ fv1 = tracker_bizobj.MakeFieldValue(
+ 1, 45, None, None, None, None, False, phase_id=1)
+ fv2 = tracker_bizobj.MakeFieldValue(
+ 1, 46, None, None, None, None, False, phase_id=2)
+ fv3 = tracker_bizobj.MakeFieldValue(
+ 1, 47, None, None, None, None, False, phase_id=1) # should replace fv4
+
+ # Adding one replaces all values since the field is single-valued.
+ merged_fvs, fvs_added_dict, fvs_removed_dict = tracker_bizobj._MergeFields(
+ [fv1, fv2], [fv3], [], [fd])
+ self.assertEqual([fv2, fv3], merged_fvs)
+ self.assertEqual({fv3.field_id: [fv3]}, fvs_added_dict)
+ self.assertEqual({}, fvs_removed_dict)
+
+ # Removing one just removes it, does not reset.
+ merged_fvs, fvs_added_dict, fvs_removed_dict = tracker_bizobj._MergeFields(
+ [fv1, fv2], [], [fv2], [fd])
+ self.assertEqual([fv1], merged_fvs)
+ self.assertEqual({}, fvs_added_dict)
+ self.assertEqual({fv2.field_id: [fv2]}, fvs_removed_dict)
+
+ def testMergeFields_MultiValued(self):
+ fd = tracker_pb2.FieldDef(
+ field_id=1, field_name='foo', is_multivalued=True)
+ fv1 = tracker_bizobj.MakeFieldValue(1, 42, None, None, None, None, False)
+ fv2 = tracker_bizobj.MakeFieldValue(1, 43, None, None, None, None, False)
+ fv3 = tracker_bizobj.MakeFieldValue(1, 44, None, None, None, None, False)
+ fv4 = tracker_bizobj.MakeFieldValue(1, 42, None, None, None, None, False)
+ fv5 = tracker_bizobj.MakeFieldValue(1, 99, None, None, None, None, False)
+ fv6 = tracker_bizobj.MakeFieldValue(1, 100, None, None, None, None, False)
+
+ merged_fvs, fvs_added_dict, fvs_removed_dict = tracker_bizobj._MergeFields(
+ [fv1, fv2], [fv2, fv3, fv6], [fv4, fv5], [fd])
+ self.assertEqual([fv2, fv3, fv6], merged_fvs)
+ self.assertEqual({fv3.field_id: [fv3, fv6]}, fvs_added_dict)
+ self.assertEqual({fv4.field_id: [fv4]}, fvs_removed_dict)
+
+ def testMergeFields_MultiValuedPhase(self):
+ fd = tracker_pb2.FieldDef(
+ field_id=1, field_name='foo', is_multivalued=True, is_phase_field=True)
+ fd2 = tracker_pb2.FieldDef(
+ field_id=2, field_name='cow', is_multivalued=True, is_phase_field=True)
+ fv1 = tracker_bizobj.MakeFieldValue(
+ 1, 42, None, None, None, None, False, phase_id=1)
+ fv2 = tracker_bizobj.MakeFieldValue(
+ 1, 43, None, None, None, None, False, phase_id=2)
+ fv3 = tracker_bizobj.MakeFieldValue(
+ 1, 44, None, None, None, None, False, phase_id=1)
+ fv4 = tracker_bizobj.MakeFieldValue(
+ 1, 99, None, None, None, None, False, phase_id=2)
+ fv5 = tracker_bizobj.MakeFieldValue(
+ 2, 22, None, None, None, None, False, phase_id=2)
+
+ merged_fvs, fvs_added_dict, fvs_removed_dict = tracker_bizobj._MergeFields(
+ [fv1, fv2], [fv3, fv1, fv5], [fv2, fv4], [fd, fd2])
+ self.assertEqual([fv1, fv3, fv5], merged_fvs)
+ self.assertEqual({fv3.field_id: [fv3], fv5.field_id: [fv5]}, fvs_added_dict)
+ self.assertEqual({fv2.field_id: [fv2]}, fvs_removed_dict)
+
+ def testSplitBlockedOnRanks_Normal(self):
+ issue = tracker_pb2.Issue()
+ issue.blocked_on_iids = [78902, 78903, 78904]
+ issue.blocked_on_ranks = [10, 20, 30]
+ rank_rows = list(zip(issue.blocked_on_iids, issue.blocked_on_ranks))
+ rank_rows.reverse()
+ ret = tracker_bizobj.SplitBlockedOnRanks(
+ issue, 78903, False, issue.blocked_on_iids)
+ self.assertEqual(ret, (rank_rows[:1], rank_rows[1:]))
+
+ def testSplitBlockedOnRanks_BadTarget(self):
+ issue = tracker_pb2.Issue()
+ issue.blocked_on_iids = [78902, 78903, 78904]
+ issue.blocked_on_ranks = [10, 20, 30]
+ rank_rows = list(zip(issue.blocked_on_iids, issue.blocked_on_ranks))
+ rank_rows.reverse()
+ ret = tracker_bizobj.SplitBlockedOnRanks(
+ issue, 78999, False, issue.blocked_on_iids)
+ self.assertEqual(ret, (rank_rows, []))
diff --git a/tracker/test/tracker_helpers_test.py b/tracker/test/tracker_helpers_test.py
new file mode 100644
index 0000000..4f89cc9
--- /dev/null
+++ b/tracker/test/tracker_helpers_test.py
@@ -0,0 +1,2775 @@
+# Copyright 2016 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
+
+"""Unittest for the tracker helpers module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import copy
+import mock
+import unittest
+
+import settings
+
+from businesslogic import work_env
+from framework import exceptions
+from framework import framework_constants
+from framework import framework_helpers
+from framework import permissions
+from framework import template_helpers
+from framework import urls
+from proto import project_pb2
+from proto import tracker_pb2
+from proto import user_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+from tracker import tracker_helpers
+
+TEST_ID_MAP = {
+ 'a@example.com': 1,
+ 'b@example.com': 2,
+ 'c@example.com': 3,
+ 'd@example.com': 4,
+ }
+
+
+def _Issue(project_name, local_id, summary='', status='', project_id=789):
+ issue = tracker_pb2.Issue()
+ issue.project_name = project_name
+ issue.project_id = project_id
+ issue.local_id = local_id
+ issue.issue_id = 100000 + local_id
+ issue.summary = summary
+ issue.status = status
+ return issue
+
+
+def _MakeConfig():
+ config = tracker_pb2.ProjectIssueConfig()
+ config.well_known_statuses.append(tracker_pb2.StatusDef(
+ means_open=True, status='New', deprecated=False))
+ config.well_known_statuses.append(tracker_pb2.StatusDef(
+ status='Old', means_open=False, deprecated=False))
+ config.well_known_statuses.append(tracker_pb2.StatusDef(
+ status='StatusThatWeDontUseAnymore', means_open=False, deprecated=True))
+
+ return config
+
+
+class HelpersTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+
+ for email, user_id in TEST_ID_MAP.items():
+ self.services.user.TestAddUser(email, user_id)
+
+ self.services.project.TestAddProject('testproj', project_id=789)
+ self.issue1 = fake.MakeTestIssue(789, 1, 'one', 'New', 111)
+ self.issue1.project_name = 'testproj'
+ self.services.issue.TestAddIssue(self.issue1)
+ self.issue2 = fake.MakeTestIssue(789, 2, 'two', 'New', 111)
+ self.issue2.project_name = 'testproj'
+ self.services.issue.TestAddIssue(self.issue2)
+ self.issue3 = fake.MakeTestIssue(789, 3, 'three', 'New', 111)
+ self.issue3.project_name = 'testproj'
+ self.services.issue.TestAddIssue(self.issue3)
+ self.cnxn = 'fake connextion'
+ self.errors = template_helpers.EZTError()
+ self.default_colspec_param = 'colspec=%s' % (
+ tracker_constants.DEFAULT_COL_SPEC.replace(' ', '%20'))
+ self.services.usergroup.TestAddGroupSettings(999, 'group@example.com')
+
+ def testParseIssueRequest_Empty(self):
+ post_data = fake.PostData()
+ errors = template_helpers.EZTError()
+ parsed = tracker_helpers.ParseIssueRequest(
+ 'fake cnxn', post_data, self.services, errors, 'proj')
+ self.assertEqual('', parsed.summary)
+ self.assertEqual('', parsed.comment)
+ self.assertEqual('', parsed.status)
+ self.assertEqual('', parsed.users.owner_username)
+ self.assertEqual(0, parsed.users.owner_id)
+ self.assertEqual([], parsed.users.cc_usernames)
+ self.assertEqual([], parsed.users.cc_usernames_remove)
+ self.assertEqual([], parsed.users.cc_ids)
+ self.assertEqual([], parsed.users.cc_ids_remove)
+ self.assertEqual('', parsed.template_name)
+ self.assertEqual([], parsed.labels)
+ self.assertEqual([], parsed.labels_remove)
+ self.assertEqual({}, parsed.fields.vals)
+ self.assertEqual({}, parsed.fields.vals_remove)
+ self.assertEqual([], parsed.fields.fields_clear)
+ self.assertEqual('', parsed.blocked_on.entered_str)
+ self.assertEqual([], parsed.blocked_on.iids)
+
+ def testParseIssueRequest_Normal(self):
+ post_data = fake.PostData({
+ 'summary': ['some summary'],
+ 'comment': ['some comment'],
+ 'status': ['SomeStatus'],
+ 'template_name': ['some template'],
+ 'label': ['lab1', '-lab2'],
+ 'custom_123': ['field1123a', 'field1123b'],
+ })
+ errors = template_helpers.EZTError()
+ parsed = tracker_helpers.ParseIssueRequest(
+ 'fake cnxn', post_data, self.services, errors, 'proj')
+ self.assertEqual('some summary', parsed.summary)
+ self.assertEqual('some comment', parsed.comment)
+ self.assertEqual('SomeStatus', parsed.status)
+ self.assertEqual('', parsed.users.owner_username)
+ self.assertEqual(0, parsed.users.owner_id)
+ self.assertEqual([], parsed.users.cc_usernames)
+ self.assertEqual([], parsed.users.cc_usernames_remove)
+ self.assertEqual([], parsed.users.cc_ids)
+ self.assertEqual([], parsed.users.cc_ids_remove)
+ self.assertEqual('some template', parsed.template_name)
+ self.assertEqual(['lab1'], parsed.labels)
+ self.assertEqual(['lab2'], parsed.labels_remove)
+ self.assertEqual({123: ['field1123a', 'field1123b']}, parsed.fields.vals)
+ self.assertEqual({}, parsed.fields.vals_remove)
+ self.assertEqual([], parsed.fields.fields_clear)
+
+ def testMarkupDescriptionOnInput(self):
+ content = 'What?\nthat\nWhy?\nidk\nWhere?\n'
+ tmpl_txt = 'What?\nWhy?\nWhere?\nWhen?'
+ desc = '<b>What?</b>\nthat\n<b>Why?</b>\nidk\n<b>Where?</b>\n'
+ self.assertEqual(tracker_helpers.MarkupDescriptionOnInput(
+ content, tmpl_txt), desc)
+
+ def testMarkupDescriptionLineOnInput(self):
+ line = 'What happened??'
+ tmpl_lines = ['What happened??','Why?']
+ self.assertEqual(tracker_helpers._MarkupDescriptionLineOnInput(
+ line, tmpl_lines), '<b>What happened??</b>')
+
+ line = 'Something terrible!!!'
+ self.assertEqual(tracker_helpers._MarkupDescriptionLineOnInput(
+ line, tmpl_lines), 'Something terrible!!!')
+
+ def testClassifyPlusMinusItems(self):
+ add, remove = tracker_helpers._ClassifyPlusMinusItems([])
+ self.assertEqual([], add)
+ self.assertEqual([], remove)
+
+ add, remove = tracker_helpers._ClassifyPlusMinusItems(
+ ['', ' ', ' \t', '-'])
+ self.assertItemsEqual([], add)
+ self.assertItemsEqual([], remove)
+
+ add, remove = tracker_helpers._ClassifyPlusMinusItems(
+ ['a', 'b', 'c'])
+ self.assertItemsEqual(['a', 'b', 'c'], add)
+ self.assertItemsEqual([], remove)
+
+ add, remove = tracker_helpers._ClassifyPlusMinusItems(
+ ['a-a-a', 'b-b', 'c-'])
+ self.assertItemsEqual(['a-a-a', 'b-b', 'c-'], add)
+ self.assertItemsEqual([], remove)
+
+ add, remove = tracker_helpers._ClassifyPlusMinusItems(
+ ['-a'])
+ self.assertItemsEqual([], add)
+ self.assertItemsEqual(['a'], remove)
+
+ add, remove = tracker_helpers._ClassifyPlusMinusItems(
+ ['-a', 'b', 'c-c'])
+ self.assertItemsEqual(['b', 'c-c'], add)
+ self.assertItemsEqual(['a'], remove)
+
+ add, remove = tracker_helpers._ClassifyPlusMinusItems(
+ ['-a', '-b-b', '-c-'])
+ self.assertItemsEqual([], add)
+ self.assertItemsEqual(['a', 'b-b', 'c-'], remove)
+
+ # We dedup, but we don't cancel out items that are both added and removed.
+ add, remove = tracker_helpers._ClassifyPlusMinusItems(
+ ['a', 'a', '-a'])
+ self.assertItemsEqual(['a'], add)
+ self.assertItemsEqual(['a'], remove)
+
+ def testParseIssueRequestFields(self):
+ parsed_fields = tracker_helpers._ParseIssueRequestFields(fake.PostData({
+ 'custom_1': ['https://hello.com'],
+ 'custom_12': ['https://blah.com'],
+ 'custom_14': ['https://remove.com'],
+ 'custom_15_goats': ['2', '3'],
+ 'custom_15_sheep': ['3', '5'],
+ 'custom_16_sheep': ['yarn'],
+ 'op_custom_14': ['remove'],
+ 'op_custom_12': ['clear'],
+ 'op_custom_16_sheep': ['remove'],
+ 'ignore': 'no matter',}))
+ self.assertEqual(
+ parsed_fields,
+ tracker_helpers.ParsedFields(
+ {
+ 1: ['https://hello.com'],
+ 12: ['https://blah.com']
+ }, {14: ['https://remove.com']}, [12],
+ {15: {
+ 'goats': ['2', '3'],
+ 'sheep': ['3', '5']
+ }}, {16: {
+ 'sheep': ['yarn']
+ }}))
+
+ def testParseIssueRequestAttachments(self):
+ file1 = testing_helpers.Blank(
+ filename='hello.c',
+ value='hello world')
+
+ file2 = testing_helpers.Blank(
+ filename='README',
+ value='Welcome to our project')
+
+ file3 = testing_helpers.Blank(
+ filename='c:\\dir\\subdir\\FILENAME.EXT',
+ value='Abort, Retry, or Fail?')
+
+ # Browsers send this if FILE field was not filled in.
+ file4 = testing_helpers.Blank(
+ filename='',
+ value='')
+
+ attachments = tracker_helpers._ParseIssueRequestAttachments({})
+ self.assertEqual([], attachments)
+
+ attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
+ 'file1': [file1],
+ }))
+ self.assertEqual(
+ [('hello.c', 'hello world', 'text/plain')],
+ attachments)
+
+ attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
+ 'file1': [file1],
+ 'file2': [file2],
+ }))
+ self.assertEqual(
+ [('hello.c', 'hello world', 'text/plain'),
+ ('README', 'Welcome to our project', 'text/plain')],
+ attachments)
+
+ attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
+ 'file3': [file3],
+ }))
+ self.assertEqual(
+ [('FILENAME.EXT', 'Abort, Retry, or Fail?',
+ 'application/octet-stream')],
+ attachments)
+
+ attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
+ 'file1': [file4], # Does not appear in result
+ 'file3': [file3],
+ 'file4': [file4], # Does not appear in result
+ }))
+ self.assertEqual(
+ [('FILENAME.EXT', 'Abort, Retry, or Fail?',
+ 'application/octet-stream')],
+ attachments)
+
+ def testParseIssueRequestKeptAttachments(self):
+ pass # TODO(jrobbins): Write this test.
+
+ def testParseIssueRequestUsers(self):
+ post_data = {}
+ parsed_users = tracker_helpers._ParseIssueRequestUsers(
+ 'fake connection', post_data, self.services)
+ self.assertEqual('', parsed_users.owner_username)
+ self.assertEqual(
+ framework_constants.NO_USER_SPECIFIED, parsed_users.owner_id)
+ self.assertEqual([], parsed_users.cc_usernames)
+ self.assertEqual([], parsed_users.cc_usernames_remove)
+ self.assertEqual([], parsed_users.cc_ids)
+ self.assertEqual([], parsed_users.cc_ids_remove)
+
+ post_data = fake.PostData({
+ 'owner': [''],
+ })
+ parsed_users = tracker_helpers._ParseIssueRequestUsers(
+ 'fake connection', post_data, self.services)
+ self.assertEqual('', parsed_users.owner_username)
+ self.assertEqual(
+ framework_constants.NO_USER_SPECIFIED, parsed_users.owner_id)
+ self.assertEqual([], parsed_users.cc_usernames)
+ self.assertEqual([], parsed_users.cc_usernames_remove)
+ self.assertEqual([], parsed_users.cc_ids)
+ self.assertEqual([], parsed_users.cc_ids_remove)
+
+ post_data = fake.PostData({
+ 'owner': [' \t'],
+ })
+ parsed_users = tracker_helpers._ParseIssueRequestUsers(
+ 'fake connection', post_data, self.services)
+ self.assertEqual('', parsed_users.owner_username)
+ self.assertEqual(
+ framework_constants.NO_USER_SPECIFIED, parsed_users.owner_id)
+ self.assertEqual([], parsed_users.cc_usernames)
+ self.assertEqual([], parsed_users.cc_usernames_remove)
+ self.assertEqual([], parsed_users.cc_ids)
+ self.assertEqual([], parsed_users.cc_ids_remove)
+
+ post_data = fake.PostData({
+ 'owner': ['b@example.com'],
+ })
+ parsed_users = tracker_helpers._ParseIssueRequestUsers(
+ 'fake connection', post_data, self.services)
+ self.assertEqual('b@example.com', parsed_users.owner_username)
+ self.assertEqual(TEST_ID_MAP['b@example.com'], parsed_users.owner_id)
+ self.assertEqual([], parsed_users.cc_usernames)
+ self.assertEqual([], parsed_users.cc_usernames_remove)
+ self.assertEqual([], parsed_users.cc_ids)
+ self.assertEqual([], parsed_users.cc_ids_remove)
+
+ post_data = fake.PostData({
+ 'owner': ['b@example.com'],
+ })
+ parsed_users = tracker_helpers._ParseIssueRequestUsers(
+ 'fake connection', post_data, self.services)
+ self.assertEqual('b@example.com', parsed_users.owner_username)
+ self.assertEqual(TEST_ID_MAP['b@example.com'], parsed_users.owner_id)
+ self.assertEqual([], parsed_users.cc_usernames)
+ self.assertEqual([], parsed_users.cc_usernames_remove)
+ self.assertEqual([], parsed_users.cc_ids)
+ self.assertEqual([], parsed_users.cc_ids_remove)
+
+ post_data = fake.PostData({
+ 'cc': ['b@example.com'],
+ })
+ parsed_users = tracker_helpers._ParseIssueRequestUsers(
+ 'fake connection', post_data, self.services)
+ self.assertEqual('', parsed_users.owner_username)
+ self.assertEqual(
+ framework_constants.NO_USER_SPECIFIED, parsed_users.owner_id)
+ self.assertEqual(['b@example.com'], parsed_users.cc_usernames)
+ self.assertEqual([], parsed_users.cc_usernames_remove)
+ self.assertEqual([TEST_ID_MAP['b@example.com']], parsed_users.cc_ids)
+ self.assertEqual([], parsed_users.cc_ids_remove)
+
+ post_data = fake.PostData({
+ 'cc': ['-b@example.com, c@example.com,,'
+ 'a@example.com,'],
+ })
+ parsed_users = tracker_helpers._ParseIssueRequestUsers(
+ 'fake connection', post_data, self.services)
+ self.assertEqual('', parsed_users.owner_username)
+ self.assertEqual(
+ framework_constants.NO_USER_SPECIFIED, parsed_users.owner_id)
+ self.assertItemsEqual(['c@example.com', 'a@example.com'],
+ parsed_users.cc_usernames)
+ self.assertEqual(['b@example.com'], parsed_users.cc_usernames_remove)
+ self.assertItemsEqual([TEST_ID_MAP['c@example.com'],
+ TEST_ID_MAP['a@example.com']],
+ parsed_users.cc_ids)
+ self.assertEqual([TEST_ID_MAP['b@example.com']],
+ parsed_users.cc_ids_remove)
+
+ post_data = fake.PostData({
+ 'owner': ['fuhqwhgads@example.com'],
+ 'cc': ['c@example.com, fuhqwhgads@example.com'],
+ })
+ parsed_users = tracker_helpers._ParseIssueRequestUsers(
+ 'fake connection', post_data, self.services)
+ self.assertEqual('fuhqwhgads@example.com', parsed_users.owner_username)
+ gen_uid = framework_helpers.MurmurHash3_x86_32(parsed_users.owner_username)
+ self.assertEqual(gen_uid, parsed_users.owner_id) # autocreated user
+ self.assertItemsEqual(
+ ['c@example.com', 'fuhqwhgads@example.com'], parsed_users.cc_usernames)
+ self.assertEqual([], parsed_users.cc_usernames_remove)
+ self.assertItemsEqual(
+ [TEST_ID_MAP['c@example.com'], gen_uid], parsed_users.cc_ids)
+ self.assertEqual([], parsed_users.cc_ids_remove)
+
+ post_data = fake.PostData({
+ 'cc': ['C@example.com, b@exAmple.cOm'],
+ })
+ parsed_users = tracker_helpers._ParseIssueRequestUsers(
+ 'fake connection', post_data, self.services)
+ self.assertItemsEqual(
+ ['c@example.com', 'b@example.com'], parsed_users.cc_usernames)
+ self.assertEqual([], parsed_users.cc_usernames_remove)
+ self.assertItemsEqual(
+ [TEST_ID_MAP['c@example.com'], TEST_ID_MAP['b@example.com']],
+ parsed_users.cc_ids)
+ self.assertEqual([], parsed_users.cc_ids_remove)
+
+ def testParseBlockers_BlockedOnNothing(self):
+ """Was blocked on nothing, still nothing."""
+ post_data = {tracker_helpers.BLOCKED_ON: ''}
+ parsed_blockers = tracker_helpers._ParseBlockers(
+ self.cnxn, post_data, self.services, self.errors, 'testproj',
+ tracker_helpers.BLOCKED_ON)
+
+ self.assertEqual('', parsed_blockers.entered_str)
+ self.assertEqual([], parsed_blockers.iids)
+ self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
+ self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKING))
+
+ def testParseBlockers_BlockedOnAdded(self):
+ """Was blocked on nothing; now 1, 2, 3."""
+ post_data = {tracker_helpers.BLOCKED_ON: '1, 2, 3'}
+ parsed_blockers = tracker_helpers._ParseBlockers(
+ self.cnxn, post_data, self.services, self.errors, 'testproj',
+ tracker_helpers.BLOCKED_ON)
+
+ self.assertEqual('1, 2, 3', parsed_blockers.entered_str)
+ self.assertEqual([100001, 100002, 100003], parsed_blockers.iids)
+ self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
+ self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKING))
+
+ def testParseBlockers_BlockedOnDuplicateRef(self):
+ """Was blocked on nothing; now just 2, but repeated in input."""
+ post_data = {tracker_helpers.BLOCKED_ON: '2, 2, 2'}
+ parsed_blockers = tracker_helpers._ParseBlockers(
+ self.cnxn, post_data, self.services, self.errors, 'testproj',
+ tracker_helpers.BLOCKED_ON)
+
+ self.assertEqual('2, 2, 2', parsed_blockers.entered_str)
+ self.assertEqual([100002], parsed_blockers.iids)
+ self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
+ self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKING))
+
+ def testParseBlockers_Missing(self):
+ """Parsing an input field that was not in the POST."""
+ post_data = {}
+ parsed_blockers = tracker_helpers._ParseBlockers(
+ self.cnxn, post_data, self.services, self.errors, 'testproj',
+ tracker_helpers.BLOCKED_ON)
+
+ self.assertEqual('', parsed_blockers.entered_str)
+ self.assertEqual([], parsed_blockers.iids)
+ self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
+ self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKING))
+
+ def testParseBlockers_SameIssueNoProject(self):
+ """Adding same issue as blocker should modify the errors object."""
+ post_data = {'id': '2', tracker_helpers.BLOCKING: '2, 3'}
+
+ parsed_blockers = tracker_helpers._ParseBlockers(
+ self.cnxn, post_data, self.services, self.errors, 'testproj',
+ tracker_helpers.BLOCKING)
+ self.assertEqual('2, 3', parsed_blockers.entered_str)
+ self.assertEqual([], parsed_blockers.iids)
+ self.assertEqual(
+ getattr(self.errors, tracker_helpers.BLOCKING),
+ 'Cannot be blocking the same issue')
+ self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
+
+ def testParseBlockers_SameIssueSameProject(self):
+ """Adding same issue as blocker should modify the errors object."""
+ post_data = {'id': '2', tracker_helpers.BLOCKING: 'testproj:2, 3'}
+
+ parsed_blockers = tracker_helpers._ParseBlockers(
+ self.cnxn, post_data, self.services, self.errors, 'testproj',
+ tracker_helpers.BLOCKING)
+ self.assertEqual('testproj:2, 3', parsed_blockers.entered_str)
+ self.assertEqual([], parsed_blockers.iids)
+ self.assertEqual(
+ getattr(self.errors, tracker_helpers.BLOCKING),
+ 'Cannot be blocking the same issue')
+ self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
+
+ def testParseBlockers_SameIssueDifferentProject(self):
+ """Adding different blocker issue should not modify the errors object."""
+ post_data = {'id': '2', tracker_helpers.BLOCKING: 'testproj:2'}
+
+ parsed_blockers = tracker_helpers._ParseBlockers(
+ self.cnxn, post_data, self.services, self.errors, 'testprojB',
+ tracker_helpers.BLOCKING)
+ self.assertEqual('testproj:2', parsed_blockers.entered_str)
+ self.assertEqual([100002], parsed_blockers.iids)
+ self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKING))
+ self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
+
+ def testParseBlockers_Invalid(self):
+ """Input fields with invalid values should modify the errors object."""
+ post_data = {tracker_helpers.BLOCKING: '2, foo',
+ tracker_helpers.BLOCKED_ON: '3, bar'}
+
+ parsed_blockers = tracker_helpers._ParseBlockers(
+ self.cnxn, post_data, self.services, self.errors, 'testproj',
+ tracker_helpers.BLOCKING)
+ self.assertEqual('2, foo', parsed_blockers.entered_str)
+ self.assertEqual([100002], parsed_blockers.iids)
+ self.assertEqual(
+ getattr(self.errors, tracker_helpers.BLOCKING), 'Invalid issue ID foo')
+ self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
+
+ parsed_blockers = tracker_helpers._ParseBlockers(
+ self.cnxn, post_data, self.services, self.errors, 'testproj',
+ tracker_helpers.BLOCKED_ON)
+ self.assertEqual('3, bar', parsed_blockers.entered_str)
+ self.assertEqual([100003], parsed_blockers.iids)
+ self.assertEqual(
+ getattr(self.errors, tracker_helpers.BLOCKED_ON),
+ 'Invalid issue ID bar')
+
+ def testParseBlockers_Dangling(self):
+ """A ref to a sanctioned projected should be allowed."""
+ post_data = {'id': '2', tracker_helpers.BLOCKING: 'otherproj:2'}
+ real_codesite_projects = settings.recognized_codesite_projects
+ settings.recognized_codesite_projects = ['otherproj']
+ parsed_blockers = tracker_helpers._ParseBlockers(
+ self.cnxn, post_data, self.services, self.errors, 'testproj',
+ tracker_helpers.BLOCKING)
+ self.assertEqual('otherproj:2', parsed_blockers.entered_str)
+ self.assertEqual([('otherproj', 2)], parsed_blockers.dangling_refs)
+ settings.recognized_codesite_projects = real_codesite_projects
+
+ def testParseBlockers_FederatedReferences(self):
+ """Should parse and return FedRefs."""
+ post_data = {'id': '9', tracker_helpers.BLOCKING: '2, b/123, 3, b/789'}
+ parsed_blockers = tracker_helpers._ParseBlockers(
+ self.cnxn, post_data, self.services, self.errors, 'testproj',
+ tracker_helpers.BLOCKING)
+ self.assertEqual('2, b/123, 3, b/789', parsed_blockers.entered_str)
+ self.assertEqual([100002, 100003], parsed_blockers.iids)
+ self.assertEqual(['b/123', 'b/789'], parsed_blockers.federated_ref_strings)
+
+ def testIsValidIssueOwner(self):
+ project = project_pb2.Project()
+ project.owner_ids.extend([1, 2])
+ project.committer_ids.extend([3])
+ project.contributor_ids.extend([4, 999])
+
+ valid, _ = tracker_helpers.IsValidIssueOwner(
+ 'fake cnxn', project, framework_constants.NO_USER_SPECIFIED,
+ self.services)
+ self.assertTrue(valid)
+
+ valid, _ = tracker_helpers.IsValidIssueOwner(
+ 'fake cnxn', project, 1,
+ self.services)
+ self.assertTrue(valid)
+ valid, _ = tracker_helpers.IsValidIssueOwner(
+ 'fake cnxn', project, 2,
+ self.services)
+ self.assertTrue(valid)
+ valid, _ = tracker_helpers.IsValidIssueOwner(
+ 'fake cnxn', project, 3,
+ self.services)
+ self.assertTrue(valid)
+ valid, _ = tracker_helpers.IsValidIssueOwner(
+ 'fake cnxn', project, 4,
+ self.services)
+ self.assertTrue(valid)
+
+ valid, _ = tracker_helpers.IsValidIssueOwner(
+ 'fake cnxn', project, 7,
+ self.services)
+ self.assertFalse(valid)
+
+ valid, _ = tracker_helpers.IsValidIssueOwner(
+ 'fake cnxn', project, 999,
+ self.services)
+ self.assertFalse(valid)
+
+ # MakeViewsForUsersInIssuesTest is tested in MakeViewsForUsersInIssuesTest.
+
+ def testGetAllowedOpenedAndClosedIssues(self):
+ pass # TOOD(jrobbins): Write this test.
+
+ def testFormatIssueListURL_JumpedToIssue(self):
+ """If we jumped to issue 123, the list is can=1&q=id-123."""
+ config = tracker_pb2.ProjectIssueConfig()
+ path = '/p/proj/issues/detail?id=123&q=123'
+ mr = testing_helpers.MakeMonorailRequest(
+ path=path, headers={'Host': 'code.google.com'})
+ mr.ComputeColSpec(config)
+
+ absolute_base_url = 'http://code.google.com'
+
+ url_1 = tracker_helpers.FormatIssueListURL(mr, config)
+ self.assertEqual(
+ '%s/p/proj/issues/list?can=1&%s&q=id%%3D123' % (
+ absolute_base_url, self.default_colspec_param),
+ url_1)
+
+ def testFormatIssueListURL_NoCurrentState(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ path = '/p/proj/issues/detail?id=123'
+ mr = testing_helpers.MakeMonorailRequest(
+ path=path, headers={'Host': 'code.google.com'})
+ mr.ComputeColSpec(config)
+
+ absolute_base_url = 'http://code.google.com'
+
+ url_1 = tracker_helpers.FormatIssueListURL(mr, config)
+ self.assertEqual(
+ '%s/p/proj/issues/list?%s&q=' % (
+ absolute_base_url, self.default_colspec_param),
+ url_1)
+
+ url_2 = tracker_helpers.FormatIssueListURL(
+ mr, config, foo=123)
+ self.assertEqual(
+ '%s/p/proj/issues/list?%s&foo=123&q=' % (
+ absolute_base_url, self.default_colspec_param),
+ url_2)
+
+ url_3 = tracker_helpers.FormatIssueListURL(
+ mr, config, foo=123, bar='abc')
+ self.assertEqual(
+ '%s/p/proj/issues/list?bar=abc&%s&foo=123&q=' % (
+ absolute_base_url, self.default_colspec_param),
+ url_3)
+
+ url_4 = tracker_helpers.FormatIssueListURL(
+ mr, config, baz='escaped+encoded&and100% "safe"')
+ self.assertEqual(
+ '%s/p/proj/issues/list?'
+ 'baz=escaped%%2Bencoded%%26and100%%25%%20%%22safe%%22&%s&q=' % (
+ absolute_base_url, self.default_colspec_param),
+ url_4)
+
+ def testFormatIssueListURL_KeepCurrentState(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ path = '/p/proj/issues/detail?id=123&sort=aa&colspec=a b c&groupby=d'
+ mr = testing_helpers.MakeMonorailRequest(
+ path=path, headers={'Host': 'localhost:8080'})
+ mr.ComputeColSpec(config)
+
+ absolute_base_url = 'http://localhost:8080'
+
+ url_1 = tracker_helpers.FormatIssueListURL(mr, config)
+ self.assertEqual(
+ '%s/p/proj/issues/list?colspec=a%%20b%%20c'
+ '&groupby=d&q=&sort=aa' % absolute_base_url,
+ url_1)
+
+ url_2 = tracker_helpers.FormatIssueListURL(
+ mr, config, foo=123)
+ self.assertEqual(
+ '%s/p/proj/issues/list?'
+ 'colspec=a%%20b%%20c&foo=123&groupby=d&q=&sort=aa' % absolute_base_url,
+ url_2)
+
+ url_3 = tracker_helpers.FormatIssueListURL(
+ mr, config, colspec='X Y Z')
+ self.assertEqual(
+ '%s/p/proj/issues/list?colspec=a%%20b%%20c'
+ '&groupby=d&q=&sort=aa' % absolute_base_url,
+ url_3)
+
+ def testFormatRelativeIssueURL(self):
+ self.assertEqual(
+ '/p/proj/issues/attachment',
+ tracker_helpers.FormatRelativeIssueURL(
+ 'proj', urls.ISSUE_ATTACHMENT))
+
+ self.assertEqual(
+ '/p/proj/issues/detail?id=123',
+ tracker_helpers.FormatRelativeIssueURL(
+ 'proj', urls.ISSUE_DETAIL, id=123))
+
+ @mock.patch('google.appengine.api.app_identity.get_application_id')
+ def testFormatCrBugURL_Prod(self, mock_get_app_id):
+ mock_get_app_id.return_value = 'monorail-prod'
+ self.assertEqual(
+ 'https://crbug.com/proj/123',
+ tracker_helpers.FormatCrBugURL('proj', 123))
+ self.assertEqual(
+ 'https://crbug.com/123456',
+ tracker_helpers.FormatCrBugURL('chromium', 123456))
+
+ @mock.patch('google.appengine.api.app_identity.get_application_id')
+ def testFormatCrBugURL_NonProd(self, mock_get_app_id):
+ mock_get_app_id.return_value = 'monorail-staging'
+ self.assertEqual(
+ '/p/proj/issues/detail?id=123',
+ tracker_helpers.FormatCrBugURL('proj', 123))
+ self.assertEqual(
+ '/p/chromium/issues/detail?id=123456',
+ tracker_helpers.FormatCrBugURL('chromium', 123456))
+
+ @mock.patch('tracker.tracker_constants.ISSUE_ATTACHMENTS_QUOTA_HARD', 1)
+ def testComputeNewQuotaBytesUsed_ProjectQuota(self):
+ upload_1 = framework_helpers.AttachmentUpload(
+ 'matter not', 'three men make a tiger', 'matter not')
+ upload_2 = framework_helpers.AttachmentUpload(
+ 'matter not', 'chicken', 'matter not')
+ attachments = [upload_1, upload_2]
+
+ project = fake.Project()
+ project.attachment_bytes_used = 10
+ project.attachment_quota = project.attachment_bytes_used + len(
+ upload_1.contents + upload_2.contents) + 1
+
+ actual_new = tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
+ expected_new = project.attachment_quota - 1
+ self.assertEqual(actual_new, expected_new)
+
+ upload_3 = framework_helpers.AttachmentUpload(
+ 'matter not', 'donut', 'matter not')
+ attachments.append(upload_3)
+ with self.assertRaises(exceptions.OverAttachmentQuota):
+ tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
+
+ @mock.patch(
+ 'tracker.tracker_constants.ISSUE_ATTACHMENTS_QUOTA_HARD', len('tiger'))
+ def testComputeNewQuotaBytesUsed_GeneralQuota(self):
+ upload_1 = framework_helpers.AttachmentUpload(
+ 'matter not', 'tiger', 'matter not')
+ attachments = [upload_1]
+
+ project = fake.Project()
+
+ actual_new = tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
+ expected_new = len(upload_1.contents)
+ self.assertEqual(actual_new, expected_new)
+
+ upload_2 = framework_helpers.AttachmentUpload(
+ 'matter not', 'donut', 'matter not')
+ attachments.append(upload_2)
+ with self.assertRaises(exceptions.OverAttachmentQuota):
+ tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
+
+ upload_3 = framework_helpers.AttachmentUpload(
+ 'matter not', 'donut', 'matter not')
+ attachments.append(upload_3)
+ with self.assertRaises(exceptions.OverAttachmentQuota):
+ tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
+
+ def testIsUnderSoftAttachmentQuota(self):
+ pass # TODO(jrobbins): Write this test.
+
+ # GetAllIssueProjects is tested in GetAllIssueProjectsTest.
+
+ def testGetPermissionsInAllProjects(self):
+ pass # TODO(jrobbins): Write this test.
+
+ # FilterOutNonViewableIssues is tested in FilterOutNonViewableIssuesTest.
+
+ def testMeansOpenInProject(self):
+ config = _MakeConfig()
+
+ # ensure open means open
+ self.assertTrue(tracker_helpers.MeansOpenInProject('New', config))
+ self.assertTrue(tracker_helpers.MeansOpenInProject('new', config))
+
+ # ensure an unrecognized status means open
+ self.assertTrue(tracker_helpers.MeansOpenInProject(
+ '_undefined_status_', config))
+
+ # ensure closed means closed
+ self.assertFalse(tracker_helpers.MeansOpenInProject('Old', config))
+ self.assertFalse(tracker_helpers.MeansOpenInProject('old', config))
+ self.assertFalse(tracker_helpers.MeansOpenInProject(
+ 'StatusThatWeDontUseAnymore', config))
+
+ def testIsNoisy(self):
+ self.assertTrue(tracker_helpers.IsNoisy(778, 320))
+ self.assertFalse(tracker_helpers.IsNoisy(20, 500))
+ self.assertFalse(tracker_helpers.IsNoisy(500, 20))
+ self.assertFalse(tracker_helpers.IsNoisy(1, 1))
+
+ def testMergeCCsAndAddComment(self):
+ target_issue = fake.MakeTestIssue(
+ 789, 10, 'Target issue', 'New', 111)
+ source_issue = fake.MakeTestIssue(
+ 789, 100, 'Source issue', 'New', 222)
+ source_issue.cc_ids.append(111)
+ # Issue without owner
+ source_issue_2 = fake.MakeTestIssue(
+ 789, 101, 'Source issue 2', 'New', 0)
+
+ self.services.issue.TestAddIssue(target_issue)
+ self.services.issue.TestAddIssue(source_issue)
+ self.services.issue.TestAddIssue(source_issue_2)
+
+ # We copy this list so that it isn't updated by the test framework
+ initial_issue_comments = (
+ self.services.issue.GetCommentsForIssue(
+ 'fake cnxn', target_issue.issue_id)[:])
+ mr = testing_helpers.MakeMonorailRequest(user_info={'user_id': 111})
+
+ # Merging source into target should create a comment.
+ self.assertIsNotNone(
+ tracker_helpers.MergeCCsAndAddComment(
+ self.services, mr, source_issue, target_issue))
+ updated_issue_comments = self.services.issue.GetCommentsForIssue(
+ 'fake cnxn', target_issue.issue_id)
+ for comment in initial_issue_comments:
+ self.assertIn(comment, updated_issue_comments)
+ self.assertEqual(
+ len(initial_issue_comments) + 1, len(updated_issue_comments))
+
+ # Merging source into target should add source's owner to target's CCs.
+ updated_target_issue = self.services.issue.GetIssueByLocalID(
+ 'fake cnxn', 789, 10)
+ self.assertIn(111, updated_target_issue.cc_ids)
+ self.assertIn(222, updated_target_issue.cc_ids)
+
+ # Merging source 2 into target should make a comment, but not update CCs.
+ self.assertIsNotNone(
+ tracker_helpers.MergeCCsAndAddComment(
+ self.services, mr, source_issue_2, updated_target_issue))
+ updated_target_issue = self.services.issue.GetIssueByLocalID(
+ 'fake cnxn', 789, 10)
+ self.assertNotIn(0, updated_target_issue.cc_ids)
+
+ def testMergeCCsAndAddComment_RestrictedSourceIssue(self):
+ target_issue = fake.MakeTestIssue(
+ 789, 10, 'Target issue', 'New', 222)
+ target_issue_2 = fake.MakeTestIssue(
+ 789, 11, 'Target issue 2', 'New', 222)
+ source_issue = fake.MakeTestIssue(
+ 789, 100, 'Source issue', 'New', 111)
+ source_issue.cc_ids.append(111)
+ source_issue.labels.append('Restrict-View-Commit')
+ target_issue_2.labels.append('Restrict-View-Commit')
+
+ self.services.issue.TestAddIssue(source_issue)
+ self.services.issue.TestAddIssue(target_issue)
+ self.services.issue.TestAddIssue(target_issue_2)
+
+ # We copy this list so that it isn't updated by the test framework
+ initial_issue_comments = self.services.issue.GetCommentsForIssue(
+ 'fake cnxn', target_issue.issue_id)[:]
+ mr = testing_helpers.MakeMonorailRequest(user_info={'user_id': 111})
+ self.assertIsNotNone(
+ tracker_helpers.MergeCCsAndAddComment(
+ self.services, mr, source_issue, target_issue))
+
+ # When the source is restricted, we update the target comments...
+ updated_issue_comments = self.services.issue.GetCommentsForIssue(
+ 'fake cnxn', target_issue.issue_id)
+ for comment in initial_issue_comments:
+ self.assertIn(comment, updated_issue_comments)
+ self.assertEqual(
+ len(initial_issue_comments) + 1, len(updated_issue_comments))
+ # ...but not the target CCs...
+ updated_target_issue = self.services.issue.GetIssueByLocalID(
+ 'fake cnxn', 789, 10)
+ self.assertNotIn(111, updated_target_issue.cc_ids)
+ # ...unless both issues have the same restrictions.
+ self.assertIsNotNone(
+ tracker_helpers.MergeCCsAndAddComment(
+ self.services, mr, source_issue, target_issue_2))
+ updated_target_issue_2 = self.services.issue.GetIssueByLocalID(
+ 'fake cnxn', 789, 11)
+ self.assertIn(111, updated_target_issue_2.cc_ids)
+
+ def testMergeCCsAndAddCommentMultipleIssues(self):
+ pass # TODO(jrobbins): Write this test.
+
+ def testGetAttachmentIfAllowed(self):
+ pass # TODO(jrobbins): Write this test.
+
+ def testLabelsMaskedByFields(self):
+ pass # TODO(jrobbins): Write this test.
+
+ def testLabelsNotMaskedByFields(self):
+ pass # TODO(jrobbins): Write this test.
+
+ def testLookupComponentIDs(self):
+ pass # TODO(jrobbins): Write this test.
+
+ def testParsePostDataUsers(self):
+ pd_users = 'a@example.com, b@example.com'
+
+ pd_users_ids, pd_users_str = tracker_helpers.ParsePostDataUsers(
+ self.cnxn, pd_users, self.services.user)
+
+ self.assertEqual([1, 2], sorted(pd_users_ids))
+ self.assertEqual('a@example.com, b@example.com', pd_users_str)
+
+ def testParsePostDataUsers_Empty(self):
+ pd_users = ''
+
+ pd_users_ids, pd_users_str = tracker_helpers.ParsePostDataUsers(
+ self.cnxn, pd_users, self.services.user)
+
+ self.assertEqual([], sorted(pd_users_ids))
+ self.assertEqual('', pd_users_str)
+
+ def testFilterIssueTypes(self):
+ pass # TODO(jrobbins): Write this test.
+
+ # ParseMergeFields is tested in IssueMergeTest.
+ # AddIssueStarrers is tested in IssueMergeTest.testMergeIssueStars().
+ # IsMergeAllowed is tested in IssueMergeTest.
+
+ def testPairDerivedValuesWithRuleExplanations_Nothing(self):
+ """Test we return nothing for an issue with no derived values."""
+ proposed_issue = tracker_pb2.Issue() # No derived values.
+ traces = {}
+ derived_users_by_id = {}
+ actual = tracker_helpers.PairDerivedValuesWithRuleExplanations(
+ proposed_issue, traces, derived_users_by_id)
+ (derived_labels_and_why, derived_owner_and_why,
+ derived_cc_and_why, warnings_and_why, errors_and_why) = actual
+ self.assertEqual([], derived_labels_and_why)
+ self.assertEqual([], derived_owner_and_why)
+ self.assertEqual([], derived_cc_and_why)
+ self.assertEqual([], warnings_and_why)
+ self.assertEqual([], errors_and_why)
+
+ def testPairDerivedValuesWithRuleExplanations_SomeValues(self):
+ """Test we return derived values and explanations for an issue."""
+ proposed_issue = tracker_pb2.Issue(
+ derived_owner_id=111, derived_cc_ids=[222, 333],
+ derived_labels=['aaa', 'zzz'],
+ derived_warnings=['Watch out'],
+ derived_errors=['Status Assigned requires an owner'])
+ traces = {
+ (tracker_pb2.FieldID.OWNER, 111): 'explain 1',
+ (tracker_pb2.FieldID.CC, 222): 'explain 2',
+ (tracker_pb2.FieldID.CC, 333): 'explain 3',
+ (tracker_pb2.FieldID.LABELS, 'aaa'): 'explain 4',
+ (tracker_pb2.FieldID.WARNING, 'Watch out'): 'explain 6',
+ (tracker_pb2.FieldID.ERROR,
+ 'Status Assigned requires an owner'): 'explain 7',
+ # There can be extra traces that are not used.
+ (tracker_pb2.FieldID.LABELS, 'bbb'): 'explain 5',
+ # If there is no trace for some derived value, why is None.
+ }
+ derived_users_by_id = {
+ 111: testing_helpers.Blank(display_name='one@example.com'),
+ 222: testing_helpers.Blank(display_name='two@example.com'),
+ 333: testing_helpers.Blank(display_name='three@example.com'),
+ }
+ actual = tracker_helpers.PairDerivedValuesWithRuleExplanations(
+ proposed_issue, traces, derived_users_by_id)
+ (derived_labels_and_why, derived_owner_and_why,
+ derived_cc_and_why, warnings_and_why, errors_and_why) = actual
+ self.assertEqual([
+ {'value': 'aaa', 'why': 'explain 4'},
+ {'value': 'zzz', 'why': None},
+ ], derived_labels_and_why)
+ self.assertEqual([
+ {'value': 'one@example.com', 'why': 'explain 1'},
+ ], derived_owner_and_why)
+ self.assertEqual([
+ {'value': 'two@example.com', 'why': 'explain 2'},
+ {'value': 'three@example.com', 'why': 'explain 3'},
+ ], derived_cc_and_why)
+ self.assertEqual([
+ {'value': 'Watch out', 'why': 'explain 6'},
+ ], warnings_and_why)
+ self.assertEqual([
+ {'value': 'Status Assigned requires an owner', 'why': 'explain 7'},
+ ], errors_and_why)
+
+
+class MakeViewsForUsersInIssuesTest(unittest.TestCase):
+
+ def setUp(self):
+ self.issue1 = _Issue('proj', 1)
+ self.issue1.owner_id = 1001
+ self.issue1.reporter_id = 1002
+
+ self.issue2 = _Issue('proj', 2)
+ self.issue2.owner_id = 2001
+ self.issue2.reporter_id = 2002
+ self.issue2.cc_ids.extend([1, 1001, 1002, 1003])
+
+ self.issue3 = _Issue('proj', 3)
+ self.issue3.owner_id = 1001
+ self.issue3.reporter_id = 3002
+
+ self.user = fake.UserService()
+ for user_id in [1, 1001, 1002, 1003, 2001, 2002, 3002]:
+ self.user.TestAddUser(
+ 'test%d' % user_id, user_id, add_user=True)
+
+ def testMakeViewsForUsersInIssues(self):
+ issue_list = [self.issue1, self.issue2, self.issue3]
+ users_by_id = tracker_helpers.MakeViewsForUsersInIssues(
+ 'fake cnxn', issue_list, self.user)
+ self.assertItemsEqual([0, 1, 1001, 1002, 1003, 2001, 2002, 3002],
+ list(users_by_id.keys()))
+ for user_id in [1001, 1002, 1003, 2001]:
+ self.assertEqual(users_by_id[user_id].user_id, user_id)
+
+ def testMakeViewsForUsersInIssuesOmittingSome(self):
+ issue_list = [self.issue1, self.issue2, self.issue3]
+ users_by_id = tracker_helpers.MakeViewsForUsersInIssues(
+ 'fake cnxn', issue_list, self.user, omit_ids=[1001, 1003])
+ self.assertItemsEqual([0, 1, 1002, 2001, 2002, 3002],
+ list(users_by_id.keys()))
+ for user_id in [1002, 2001, 2002, 3002]:
+ self.assertEqual(users_by_id[user_id].user_id, user_id)
+
+ def testMakeViewsForUsersInIssuesEmpty(self):
+ issue_list = []
+ users_by_id = tracker_helpers.MakeViewsForUsersInIssues(
+ 'fake cnxn', issue_list, self.user)
+ self.assertItemsEqual([], list(users_by_id.keys()))
+
+
+class GetAllIssueProjectsTest(unittest.TestCase):
+ issue_x_1 = tracker_pb2.Issue()
+ issue_x_1.project_id = 789
+ issue_x_1.local_id = 1
+ issue_x_1.reporter_id = 1002
+
+ issue_x_2 = tracker_pb2.Issue()
+ issue_x_2.project_id = 789
+ issue_x_2.local_id = 2
+ issue_x_2.reporter_id = 2002
+
+ issue_y_1 = tracker_pb2.Issue()
+ issue_y_1.project_id = 678
+ issue_y_1.local_id = 1
+ issue_y_1.reporter_id = 2002
+
+ def setUp(self):
+ self.project_service = fake.ProjectService()
+ self.project_service.TestAddProject('proj-x', project_id=789)
+ self.project_service.TestAddProject('proj-y', project_id=678)
+ self.cnxn = 'fake connection'
+
+ def testGetAllIssueProjects_Empty(self):
+ self.assertEqual(
+ {}, tracker_helpers.GetAllIssueProjects(
+ self.cnxn, [], self.project_service))
+
+ def testGetAllIssueProjects_Normal(self):
+ self.assertEqual(
+ {789: self.project_service.GetProjectByName(self.cnxn, 'proj-x')},
+ tracker_helpers.GetAllIssueProjects(
+ self.cnxn, [self.issue_x_1, self.issue_x_2], self.project_service))
+ self.assertEqual(
+ {789: self.project_service.GetProjectByName(self.cnxn, 'proj-x'),
+ 678: self.project_service.GetProjectByName(self.cnxn, 'proj-y')},
+ tracker_helpers.GetAllIssueProjects(
+ self.cnxn, [self.issue_x_1, self.issue_x_2, self.issue_y_1],
+ self.project_service))
+
+
+class FilterOutNonViewableIssuesTest(unittest.TestCase):
+ owner_id = 111
+ committer_id = 222
+ nonmember_1_id = 1002
+ nonmember_2_id = 2002
+ nonmember_3_id = 3002
+
+ issue1 = tracker_pb2.Issue()
+ issue1.project_name = 'proj'
+ issue1.project_id = 789
+ issue1.local_id = 1
+ issue1.reporter_id = nonmember_1_id
+
+ issue2 = tracker_pb2.Issue()
+ issue2.project_name = 'proj'
+ issue2.project_id = 789
+ issue2.local_id = 2
+ issue2.reporter_id = nonmember_2_id
+ issue2.labels.extend(['foo', 'bar'])
+
+ issue3 = tracker_pb2.Issue()
+ issue3.project_name = 'proj'
+ issue3.project_id = 789
+ issue3.local_id = 3
+ issue3.reporter_id = nonmember_3_id
+ issue3.labels.extend(['restrict-view-commit'])
+
+ issue4 = tracker_pb2.Issue()
+ issue4.project_name = 'proj'
+ issue4.project_id = 789
+ issue4.local_id = 4
+ issue4.reporter_id = nonmember_3_id
+ issue4.labels.extend(['Foo', 'Restrict-View-Commit'])
+
+ def setUp(self):
+ self.user = user_pb2.User()
+ self.project = self.MakeProject(project_pb2.ProjectState.LIVE)
+ self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(
+ self.project.project_id)
+ self.project_dict = {self.project.project_id: self.project}
+ self.config_dict = {self.config.project_id: self.config}
+
+ def MakeProject(self, state):
+ p = project_pb2.Project(
+ project_id=789, project_name='proj', state=state,
+ owner_ids=[self.owner_id], committer_ids=[self.committer_id])
+ return p
+
+ def testFilterOutNonViewableIssues_Member(self):
+ # perms will be permissions.COMMITTER_ACTIVE_PERMISSIONSET
+ filtered_issues = tracker_helpers.FilterOutNonViewableIssues(
+ {self.committer_id}, self.user, self.project_dict,
+ self.config_dict,
+ [self.issue1, self.issue2, self.issue3, self.issue4])
+ self.assertListEqual([1, 2, 3, 4],
+ [issue.local_id for issue in filtered_issues])
+
+ def testFilterOutNonViewableIssues_Owner(self):
+ # perms will be permissions.OWNER_ACTIVE_PERMISSIONSET
+ filtered_issues = tracker_helpers.FilterOutNonViewableIssues(
+ {self.owner_id}, self.user, self.project_dict, self.config_dict,
+ [self.issue1, self.issue2, self.issue3, self.issue4])
+ self.assertListEqual([1, 2, 3, 4],
+ [issue.local_id for issue in filtered_issues])
+
+ def testFilterOutNonViewableIssues_Empty(self):
+ # perms will be permissions.COMMITTER_ACTIVE_PERMISSIONSET
+ filtered_issues = tracker_helpers.FilterOutNonViewableIssues(
+ {self.committer_id}, self.user, self.project_dict,
+ self.config_dict, [])
+ self.assertListEqual([], filtered_issues)
+
+ def testFilterOutNonViewableIssues_NonMember(self):
+ # perms will be permissions.READ_ONLY_PERMISSIONSET
+ filtered_issues = tracker_helpers.FilterOutNonViewableIssues(
+ {self.nonmember_1_id}, self.user, self.project_dict,
+ self.config_dict, [self.issue1, self.issue2, self.issue3, self.issue4])
+ self.assertListEqual([1, 2],
+ [issue.local_id for issue in filtered_issues])
+
+ def testFilterOutNonViewableIssues_Reporter(self):
+ # perms will be permissions.READ_ONLY_PERMISSIONSET
+ filtered_issues = tracker_helpers.FilterOutNonViewableIssues(
+ {self.nonmember_3_id}, self.user, self.project_dict,
+ self.config_dict, [self.issue1, self.issue2, self.issue3, self.issue4])
+ self.assertListEqual([1, 2, 3, 4],
+ [issue.local_id for issue in filtered_issues])
+
+
+class IssueMergeTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ project=fake.ProjectService(),
+ issue_star=fake.IssueStarService(),
+ spam=fake.SpamService()
+ )
+ self.project = self.services.project.TestAddProject('proj', project_id=987)
+ self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(
+ self.project.project_id)
+ self.project_dict = {self.project.project_id: self.project}
+ self.config_dict = {self.config.project_id: self.config}
+
+ def testParseMergeFields_NotSpecified(self):
+ issue = fake.MakeTestIssue(987, 1, 'summary', 'New', 111)
+ errors = template_helpers.EZTError()
+ post_data = {}
+
+ text, merge_into_issue = tracker_helpers.ParseMergeFields(
+ self.cnxn, None, 'proj', post_data, 'New', self.config, issue, errors)
+ self.assertEqual('', text)
+ self.assertEqual(None, merge_into_issue)
+
+ text, merge_into_issue = tracker_helpers.ParseMergeFields(
+ self.cnxn, None, 'proj', post_data, 'Duplicate', self.config, issue,
+ errors)
+ self.assertEqual('', text)
+ self.assertTrue(errors.merge_into_id)
+ self.assertEqual(None, merge_into_issue)
+
+ def testParseMergeFields_WrongStatus(self):
+ issue = fake.MakeTestIssue(987, 1, 'summary', 'New', 111)
+ errors = template_helpers.EZTError()
+ post_data = {'merge_into': '12'}
+
+ text, merge_into_issue = tracker_helpers.ParseMergeFields(
+ self.cnxn, None, 'proj', post_data, 'New', self.config, issue, errors)
+ self.assertEqual('', text)
+ self.assertEqual(None, merge_into_issue)
+
+ def testParseMergeFields_NoSuchIssue(self):
+ issue = fake.MakeTestIssue(987, 1, 'summary', 'New', 111)
+ issue.merged_into = 12
+ errors = template_helpers.EZTError()
+ post_data = {'merge_into': '12'}
+
+ text, merge_into_issue = tracker_helpers.ParseMergeFields(
+ self.cnxn, self.services, 'proj', post_data, 'Duplicate',
+ self.config, issue, errors)
+ self.assertEqual('12', text)
+ self.assertEqual(None, merge_into_issue)
+
+ def testParseMergeFields_DontSelfMerge(self):
+ issue = fake.MakeTestIssue(987, 1, 'summary', 'New', 111)
+ errors = template_helpers.EZTError()
+ post_data = {'merge_into': '1'}
+
+ text, merge_into_issue = tracker_helpers.ParseMergeFields(
+ self.cnxn, self.services, 'proj', post_data, 'Duplicate', self.config,
+ issue, errors)
+ self.assertEqual('1', text)
+ self.assertEqual(None, merge_into_issue)
+ self.assertEqual('Cannot merge issue into itself', errors.merge_into_id)
+
+ def testParseMergeFields_NewIssueToMerge(self):
+ merged_issue = fake.MakeTestIssue(
+ self.project.project_id,
+ 1,
+ 'unused_summary',
+ 'unused_status',
+ 111,
+ reporter_id=111)
+ self.services.issue.TestAddIssue(merged_issue)
+ mergee_issue = fake.MakeTestIssue(
+ self.project.project_id,
+ 2,
+ 'unused_summary',
+ 'unused_status',
+ 111,
+ reporter_id=111)
+ self.services.issue.TestAddIssue(mergee_issue)
+
+ errors = template_helpers.EZTError()
+ post_data = {'merge_into': str(mergee_issue.local_id)}
+
+ text, merge_into_issue = tracker_helpers.ParseMergeFields(
+ self.cnxn, self.services, 'proj', post_data, 'Duplicate', self.config,
+ merged_issue, errors)
+ self.assertEqual(str(mergee_issue.local_id), text)
+ self.assertEqual(mergee_issue, merge_into_issue)
+
+ def testIsMergeAllowed(self):
+ mr = testing_helpers.MakeMonorailRequest()
+ issue = fake.MakeTestIssue(987, 1, 'summary', 'New', 111)
+ issue.project_name = self.project.project_name
+
+ for (perm_set, expected_merge_allowed) in (
+ (permissions.READ_ONLY_PERMISSIONSET, False),
+ (permissions.COMMITTER_INACTIVE_PERMISSIONSET, False),
+ (permissions.COMMITTER_ACTIVE_PERMISSIONSET, True),
+ (permissions.OWNER_ACTIVE_PERMISSIONSET, True)):
+ mr.perms = perm_set
+ merge_allowed = tracker_helpers.IsMergeAllowed(issue, mr, self.services)
+ self.assertEqual(expected_merge_allowed, merge_allowed)
+
+ def testMergeIssueStars(self):
+ mr = testing_helpers.MakeMonorailRequest()
+ mr.project_name = self.project.project_name
+ mr.project = self.project
+
+ config = self.services.config.GetProjectConfig(
+ self.cnxn, self.project.project_id)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, config, 1, 1, True)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, config, 1, 2, True)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, config, 1, 3, True)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, config, 3, 3, True)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, config, 3, 6, True)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, config, 2, 3, True)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, config, 2, 4, True)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, config, 2, 5, True)
+
+ new_starrers = tracker_helpers.GetNewIssueStarrers(
+ self.cnxn, self.services, [1, 3], 2)
+ self.assertItemsEqual(new_starrers, [1, 2, 6])
+ tracker_helpers.AddIssueStarrers(
+ self.cnxn, self.services, mr, 2, self.project, new_starrers)
+ issue_2_starrers = self.services.issue_star.LookupItemStarrers(
+ self.cnxn, 2)
+ # XXX(jrobbins): these tests incorrectly mix local IDs with IIDs.
+ self.assertItemsEqual([1, 2, 3, 4, 5, 6], issue_2_starrers)
+
+
+class MergeLinkedMembersTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.services = service_manager.Services(
+ user=fake.UserService())
+ self.user1 = self.services.user.TestAddUser('one@example.com', 111)
+ self.user2 = self.services.user.TestAddUser('two@example.com', 222)
+
+ def testNoLinkedAccounts(self):
+ """When no candidate accounts are linked, they are all returned."""
+ actual = tracker_helpers._MergeLinkedMembers(
+ self.cnxn, self.services.user, [111, 222])
+ self.assertEqual([111, 222], actual)
+
+ def testSomeLinkedButNoMasking(self):
+ """If an account has linked accounts, but they are not here, keep it."""
+ self.user1.linked_child_ids = [999]
+ self.user2.linked_parent_id = 999
+ actual = tracker_helpers._MergeLinkedMembers(
+ self.cnxn, self.services.user, [111, 222])
+ self.assertEqual([111, 222], actual)
+
+ def testParentMasksChild(self):
+ """When two accounts linked, only the parent is returned."""
+ self.user2.linked_parent_id = 111
+ actual = tracker_helpers._MergeLinkedMembers(
+ self.cnxn, self.services.user, [111, 222])
+ self.assertEqual([111], actual)
+
+
+class FilterMemberDataTest(unittest.TestCase):
+
+ def setUp(self):
+ services = service_manager.Services(
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService())
+ self.owner_email = 'owner@dom.com'
+ self.committer_email = 'commit@dom.com'
+ self.contributor_email = 'contrib@dom.com'
+ self.indirect_member_email = 'ind@dom.com'
+ self.all_emails = [self.owner_email, self.committer_email,
+ self.contributor_email, self.indirect_member_email]
+ self.project = services.project.TestAddProject('proj')
+
+ def DoFiltering(self, perms, unsigned_user=False):
+ mr = testing_helpers.MakeMonorailRequest(
+ project=self.project, perms=perms)
+ if not unsigned_user:
+ mr.auth.user_id = 111
+ mr.auth.user_view = testing_helpers.Blank(domain='jrobbins.org')
+ return tracker_helpers._FilterMemberData(
+ mr, [self.owner_email], [self.committer_email],
+ [self.contributor_email], [self.indirect_member_email], mr.project)
+
+ def testUnsignedUser_NormalProject(self):
+ visible_members = self.DoFiltering(
+ permissions.READ_ONLY_PERMISSIONSET, unsigned_user=True)
+ self.assertItemsEqual(
+ [self.owner_email, self.committer_email, self.contributor_email,
+ self.indirect_member_email],
+ visible_members)
+
+ def testUnsignedUser_RestrictedProject(self):
+ self.project.only_owners_see_contributors = True
+ visible_members = self.DoFiltering(
+ permissions.READ_ONLY_PERMISSIONSET, unsigned_user=True)
+ self.assertItemsEqual(
+ [self.owner_email, self.committer_email, self.indirect_member_email],
+ visible_members)
+
+ def testOwnersAndAdminsCanSeeAll_NormalProject(self):
+ visible_members = self.DoFiltering(
+ permissions.OWNER_ACTIVE_PERMISSIONSET)
+ self.assertItemsEqual(self.all_emails, visible_members)
+
+ visible_members = self.DoFiltering(
+ permissions.ADMIN_PERMISSIONSET)
+ self.assertItemsEqual(self.all_emails, visible_members)
+
+ def testOwnersAndAdminsCanSeeAll_HubAndSpoke(self):
+ self.project.only_owners_see_contributors = True
+
+ visible_members = self.DoFiltering(
+ permissions.OWNER_ACTIVE_PERMISSIONSET)
+ self.assertItemsEqual(self.all_emails, visible_members)
+
+ visible_members = self.DoFiltering(
+ permissions.ADMIN_PERMISSIONSET)
+ self.assertItemsEqual(self.all_emails, visible_members)
+
+ visible_members = self.DoFiltering(
+ permissions.COMMITTER_ACTIVE_PERMISSIONSET)
+ self.assertItemsEqual(self.all_emails, visible_members)
+
+ def testNonOwnersCanSeeAll_NormalProject(self):
+ visible_members = self.DoFiltering(
+ permissions.COMMITTER_ACTIVE_PERMISSIONSET)
+ self.assertItemsEqual(self.all_emails, visible_members)
+
+ visible_members = self.DoFiltering(
+ permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET)
+ self.assertItemsEqual(self.all_emails, visible_members)
+
+ def testCommittersSeeOnlySameDomain_HubAndSpoke(self):
+ self.project.only_owners_see_contributors = True
+
+ visible_members = self.DoFiltering(
+ permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET)
+ self.assertItemsEqual(
+ [self.owner_email, self.committer_email, self.indirect_member_email],
+ visible_members)
+
+
+class GetLabelOptionsTest(unittest.TestCase):
+
+ @mock.patch('tracker.tracker_helpers.LabelsNotMaskedByFields')
+ def testGetLabelOptions(self, mockLabelsNotMaskedByFields):
+ mockLabelsNotMaskedByFields.return_value = []
+ config = tracker_pb2.ProjectIssueConfig()
+ custom_perms = []
+ actual = tracker_helpers.GetLabelOptions(config, custom_perms)
+ expected = [
+ {'doc': 'Only users who can edit the issue may access it',
+ 'name': 'Restrict-View-EditIssue'},
+ {'doc': 'Only users who can edit the issue may add comments',
+ 'name': 'Restrict-AddIssueComment-EditIssue'},
+ {'doc': 'Custom permission CoreTeam is needed to access',
+ 'name': 'Restrict-View-CoreTeam'}
+ ]
+ self.assertEqual(expected, actual)
+
+ def testBuildRestrictionChoices(self):
+ choices = tracker_helpers._BuildRestrictionChoices([], [], [])
+ self.assertEqual([], choices)
+
+ choices = tracker_helpers._BuildRestrictionChoices(
+ [], ['Hop', 'Jump'], [])
+ self.assertEqual([], choices)
+
+ freq = [('View', 'B', 'You need permission B to do anything'),
+ ('A', 'B', 'You need B to use A')]
+ choices = tracker_helpers._BuildRestrictionChoices(freq, [], [])
+ expected = [dict(name='Restrict-View-B',
+ doc='You need permission B to do anything'),
+ dict(name='Restrict-A-B',
+ doc='You need B to use A')]
+ self.assertListEqual(expected, choices)
+
+ extra_perms = ['Over18', 'Over21']
+ choices = tracker_helpers._BuildRestrictionChoices(
+ [], ['Drink', 'Smoke'], extra_perms)
+ expected = [dict(name='Restrict-Drink-Over18',
+ doc='Permission Over18 needed to use Drink'),
+ dict(name='Restrict-Drink-Over21',
+ doc='Permission Over21 needed to use Drink'),
+ dict(name='Restrict-Smoke-Over18',
+ doc='Permission Over18 needed to use Smoke'),
+ dict(name='Restrict-Smoke-Over21',
+ doc='Permission Over21 needed to use Smoke')]
+ self.assertListEqual(expected, choices)
+
+
+class FilterKeptAttachmentsTest(unittest.TestCase):
+ def testFilterKeptAttachments(self):
+ comments = [
+ tracker_pb2.IssueComment(
+ is_description=True,
+ attachments=[tracker_pb2.Attachment(attachment_id=1)]),
+ tracker_pb2.IssueComment(),
+ tracker_pb2.IssueComment(
+ is_description=True,
+ attachments=[
+ tracker_pb2.Attachment(attachment_id=2),
+ tracker_pb2.Attachment(attachment_id=3)]),
+ tracker_pb2.IssueComment(),
+ tracker_pb2.IssueComment(
+ approval_id=24,
+ is_description=True,
+ attachments=[tracker_pb2.Attachment(attachment_id=4)])]
+
+ filtered = tracker_helpers.FilterKeptAttachments(
+ True, [1, 2, 3, 4], comments, None)
+ self.assertEqual([2, 3], filtered)
+
+ def testApprovalDescription(self):
+ comments = [
+ tracker_pb2.IssueComment(
+ is_description=True,
+ attachments=[tracker_pb2.Attachment(attachment_id=1)]),
+ tracker_pb2.IssueComment(),
+ tracker_pb2.IssueComment(
+ is_description=True,
+ attachments=[
+ tracker_pb2.Attachment(attachment_id=2),
+ tracker_pb2.Attachment(attachment_id=3)]),
+ tracker_pb2.IssueComment(),
+ tracker_pb2.IssueComment(
+ approval_id=24,
+ is_description=True,
+ attachments=[tracker_pb2.Attachment(attachment_id=4)])]
+
+ filtered = tracker_helpers.FilterKeptAttachments(
+ True, [1, 2, 3, 4], comments, 24)
+ self.assertEqual([4], filtered)
+
+ def testNotAnIssueDescription(self):
+ comments = [
+ tracker_pb2.IssueComment(
+ is_description=True,
+ attachments=[tracker_pb2.Attachment(attachment_id=1)]),
+ tracker_pb2.IssueComment(),
+ tracker_pb2.IssueComment(
+ is_description=True,
+ attachments=[
+ tracker_pb2.Attachment(attachment_id=2),
+ tracker_pb2.Attachment(attachment_id=3)]),
+ tracker_pb2.IssueComment(),
+ tracker_pb2.IssueComment(
+ approval_id=24,
+ is_description=True,
+ attachments=[tracker_pb2.Attachment(attachment_id=4)])]
+
+ filtered = tracker_helpers.FilterKeptAttachments(
+ False, [1, 2, 3, 4], comments, None)
+ self.assertIsNone(filtered)
+
+ def testNoDescriptionsInComments(self):
+ comments = [
+ tracker_pb2.IssueComment(),
+ tracker_pb2.IssueComment()]
+
+ filtered = tracker_helpers.FilterKeptAttachments(
+ True, [1, 2, 3, 4], comments, None)
+ self.assertEqual([], filtered)
+
+ def testNoComments(self):
+ filtered = tracker_helpers.FilterKeptAttachments(
+ True, [1, 2, 3, 4], [], None)
+ self.assertEqual([], filtered)
+
+
+class EnumFieldHelpersTest(unittest.TestCase):
+
+ def test_GetEnumFieldValuesAndDocstrings(self):
+ """We can get all choices for an enum field"""
+ fd = tracker_pb2.FieldDef(
+ field_id=123,
+ project_id=1,
+ field_name='yellow',
+ field_type=tracker_pb2.FieldTypes.ENUM_TYPE)
+ ld_1 = tracker_pb2.LabelDef(
+ label='yellow-submarine', label_docstring='ld_1_docstring')
+ ld_2 = tracker_pb2.LabelDef(
+ label='yellow-tisket', label_docstring='ld_2_docstring')
+ ld_3 = tracker_pb2.LabelDef(
+ label='yellow-basket', label_docstring='ld_3_docstring')
+ ld_4 = tracker_pb2.LabelDef(
+ label='yellow', label_docstring='ld_4_docstring')
+ ld_5 = tracker_pb2.LabelDef(
+ label='not-yellow', label_docstring='ld_5_docstring')
+ ld_6 = tracker_pb2.LabelDef(
+ label='yellow-tasket',
+ label_docstring='ld_6_docstring',
+ deprecated=True)
+ config = tracker_pb2.ProjectIssueConfig(
+ default_template_for_developers=1,
+ default_template_for_users=2,
+ well_known_labels=[ld_1, ld_2, ld_3, ld_4, ld_5, ld_6])
+ actual = tracker_helpers._GetEnumFieldValuesAndDocstrings(fd, config)
+ # Expect to omit labels `yellow` and `not-yellow` due to prefix mismatch
+ # Also expect to omit label `yellow-tasket` because it's deprecated
+ expected = [
+ ('submarine', 'ld_1_docstring'), ('tisket', 'ld_2_docstring'),
+ ('basket', 'ld_3_docstring')
+ ]
+ self.assertEqual(expected, actual)
+
+
+class CreateIssueHelpersTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+ self.cnxn = 'fake cnxn'
+
+ self.project_member = self.services.user.TestAddUser(
+ 'user_1@example.com', 111)
+ self.project_group_member = self.services.user.TestAddUser(
+ 'group@example.com', 999)
+ self.project = self.services.project.TestAddProject(
+ 'proj',
+ project_id=789,
+ committer_ids=[
+ self.project_member.user_id, self.project_group_member.user_id
+ ])
+ self.no_project_user = self.services.user.TestAddUser(
+ 'user_2@example.com', 222)
+ self.config = fake.MakeTestConfig(self.project.project_id, [], [])
+ self.int_fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None, '', False,
+ False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.int_fd.max_value = 999
+ self.config.field_defs = [self.int_fd]
+ self.status_1 = tracker_pb2.StatusDef(
+ status='New', means_open=True, status_docstring='status_1 docstring')
+ self.config.well_known_statuses = [self.status_1]
+ self.component_def_1 = tracker_pb2.ComponentDef(
+ component_id=1, path='compFOO')
+ self.component_def_2 = tracker_pb2.ComponentDef(
+ component_id=2, path='deprecated', deprecated=True)
+ self.config.component_defs = [self.component_def_1, self.component_def_2]
+ self.services.config.StoreConfig('cnxn', self.config)
+ self.services.usergroup.TestAddGroupSettings(999, 'group@example.com')
+
+ def testAssertValidIssueForCreate_Valid(self):
+ input_issue = tracker_pb2.Issue(
+ summary='sum',
+ status='New',
+ owner_id=111,
+ project_id=789,
+ component_ids=[1],
+ cc_ids=[999])
+ tracker_helpers.AssertValidIssueForCreate(
+ self.cnxn, self.services, input_issue, 'nonempty description')
+
+ def testAssertValidIssueForCreate_ValidatesOwner(self):
+ input_issue = tracker_pb2.Issue(
+ summary='sum', status='New', owner_id=222, project_id=789)
+ with self.assertRaisesRegexp(exceptions.InputException,
+ 'Issue owner must be a project member'):
+ tracker_helpers.AssertValidIssueForCreate(
+ self.cnxn, self.services, input_issue, 'nonempty description')
+ input_issue.owner_id = 333
+ with self.assertRaisesRegexp(exceptions.InputException,
+ 'Issue owner user ID not found'):
+ tracker_helpers.AssertValidIssueForCreate(
+ self.cnxn, self.services, input_issue, 'nonempty description')
+ input_issue.owner_id = 999
+ with self.assertRaisesRegexp(exceptions.InputException,
+ 'Issue owner cannot be a user group'):
+ tracker_helpers.AssertValidIssueForCreate(
+ self.cnxn, self.services, input_issue, 'nonempty description')
+
+ def testAssertValidIssueForCreate_ValidatesSummary(self):
+ input_issue = tracker_pb2.Issue(
+ summary='', status='New', owner_id=111, project_id=789)
+ with self.assertRaisesRegexp(exceptions.InputException,
+ 'Summary is required'):
+ tracker_helpers.AssertValidIssueForCreate(
+ self.cnxn, self.services, input_issue, 'nonempty description')
+ input_issue.summary = ' '
+ tracker_helpers.AssertValidIssueForCreate(
+ self.cnxn, self.services, input_issue, 'nonempty description')
+
+ def testAssertValidIssueForCreate_ValidatesDescription(self):
+ input_issue = tracker_pb2.Issue(
+ summary='sum', status='New', owner_id=111, project_id=789)
+ with self.assertRaisesRegexp(exceptions.InputException,
+ 'Description is required'):
+ tracker_helpers.AssertValidIssueForCreate(
+ self.cnxn, self.services, input_issue, '')
+ tracker_helpers.AssertValidIssueForCreate(
+ self.cnxn, self.services, input_issue, ' ')
+
+ def testAssertValidIssueForCreate_ValidatesFieldDef(self):
+ fv = tracker_bizobj.MakeFieldValue(
+ self.int_fd.field_id, 1000, None, None, None, None, False)
+ input_issue = tracker_pb2.Issue(
+ summary='sum',
+ status='New',
+ owner_id=111,
+ project_id=789,
+ field_values=[fv])
+ with self.assertRaises(exceptions.InputException):
+ tracker_helpers.AssertValidIssueForCreate(
+ self.cnxn, self.services, input_issue, 'nonempty description')
+
+ def testAssertValidIssueForCreate_ValidatesStatus(self):
+ input_issue = tracker_pb2.Issue(
+ summary='sum', status='DNE_status', owner_id=111, project_id=789)
+
+ def mock_status_lookup(*_args, **_kwargs):
+ return None
+
+ self.services.config.LookupStatusID = mock_status_lookup
+ with self.assertRaisesRegexp(exceptions.InputException,
+ 'Undefined status: DNE_status'):
+ tracker_helpers.AssertValidIssueForCreate(
+ self.cnxn, self.services, input_issue, 'nonempty description')
+
+ def testAssertValidIssueForCreate_ValidatesComponents(self):
+ # Tests an undefined component.
+ input_issue = tracker_pb2.Issue(
+ summary='',
+ status='New',
+ owner_id=111,
+ project_id=789,
+ component_ids=[3])
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'Undefined or deprecated component with id: 3'):
+ tracker_helpers.AssertValidIssueForCreate(
+ self.cnxn, self.services, input_issue, 'nonempty description')
+
+ # Tests a deprecated component.
+ input_issue = tracker_pb2.Issue(
+ summary='',
+ status='New',
+ owner_id=111,
+ project_id=789,
+ component_ids=[self.component_def_2.component_id])
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'Undefined or deprecated component with id: 2'):
+ tracker_helpers.AssertValidIssueForCreate(
+ self.cnxn, self.services, input_issue, 'nonempty description')
+
+ def testAssertValidIssueForCreate_ValidatesUsers(self):
+ user_fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None, '', False,
+ False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.services.config.TestAddFieldDef(user_fd)
+
+ input_issue = tracker_pb2.Issue(
+ summary='sum',
+ status='New',
+ owner_id=111,
+ project_id=789,
+ cc_ids=[123],
+ field_values=[
+ tracker_bizobj.MakeFieldValue(
+ user_fd.field_id, None, None, 124, None, None, False)
+ ])
+ copied_issue = copy.deepcopy(input_issue)
+ with self.assertRaisesRegexp(exceptions.InputException,
+ r'users/123: .+\nusers/124: .+'):
+ tracker_helpers.AssertValidIssueForCreate(
+ self.cnxn, self.services, input_issue, 'nonempty description')
+ self.assertEqual(input_issue, copied_issue)
+
+ self.services.user.TestAddUser('a@test.com', 123)
+ self.services.user.TestAddUser('a@test.com', 124)
+ tracker_helpers.AssertValidIssueForCreate(
+ self.cnxn, self.services, input_issue, 'nonempty description')
+ self.assertEqual(input_issue, copied_issue)
+
+
+class ModifyIssuesHelpersTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ issue_star=fake.IssueStarService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+ self.cnxn = 'fake cnxn'
+
+ self.project_member = self.services.user.TestAddUser(
+ 'user_1@example.com', 111)
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=789, committer_ids=[self.project_member.user_id])
+ self.no_project_user = self.services.user.TestAddUser(
+ 'user_2@example.com', 222)
+
+ self.config = fake.MakeTestConfig(self.project.project_id, [], [])
+ self.int_fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None, '', False,
+ False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.int_fd.max_value = 999
+ self.config.field_defs = [self.int_fd]
+ self.services.config.StoreConfig('cnxn', self.config)
+
+ def testApplyAllIssueChanges(self):
+ issue_delta_pairs = []
+ no_change_iid = 78942
+
+ expected_issues_to_update = {}
+ expected_amendments = {}
+ expected_imp_amendments = {}
+ expected_old_owners = {}
+ expected_old_statuses = {}
+ expected_old_components = {}
+ expected_merged_from_add = {}
+ expected_new_starrers = {}
+
+ issue_main = _Issue('proj', 100)
+ issue_main_ref = ('proj', issue_main.local_id)
+ issue_main.owner_id = 999
+ issue_main.cc_ids = [111, 222]
+ issue_main.labels = ['dont_touch', 'remove_me']
+
+ expected_main = copy.deepcopy(issue_main)
+ expected_main.owner_id = 888
+ expected_main.cc_ids = [111, 333]
+ expected_main.labels = ['dont_touch', 'add_me']
+ expected_amendments[issue_main.issue_id] = [
+ tracker_bizobj.MakeOwnerAmendment(888, 999),
+ tracker_bizobj.MakeCcAmendment([333], [222]),
+ tracker_bizobj.MakeLabelsAmendment(['add_me'], ['remove_me'])
+ ]
+ expected_old_owners[issue_main.issue_id] = 999
+
+ # blocked_on issues changes setup.
+ bo_add = _Issue('proj', 1)
+ self.services.issue.TestAddIssue(bo_add)
+ expected_bo_add = copy.deepcopy(bo_add)
+ # All impacted issues should be fetched within ApplyAllIssueChanges
+ # directly from the DB, skipping cache with `use_cache=False` in GetIssue().
+ # So we expect these issues to have assume_stale=False.
+ expected_bo_add.assume_stale = False
+ expected_bo_add.blocking_iids = [issue_main.issue_id]
+ expected_issues_to_update[expected_bo_add.issue_id] = expected_bo_add
+ expected_imp_amendments[bo_add.issue_id] = [
+ tracker_bizobj.MakeBlockingAmendment(
+ [issue_main_ref], [], default_project_name='proj')
+ ]
+
+ bo_remove = _Issue('proj', 2)
+ bo_remove.blocking_iids = [issue_main.issue_id]
+ self.services.issue.TestAddIssue(bo_remove)
+ expected_bo_remove = copy.deepcopy(bo_remove)
+ expected_bo_remove.assume_stale = False
+ expected_bo_remove.blocking_iids = []
+ expected_issues_to_update[expected_bo_remove.issue_id] = expected_bo_remove
+ expected_imp_amendments[bo_remove.issue_id] = [
+ tracker_bizobj.MakeBlockingAmendment(
+ [], [issue_main_ref], default_project_name='proj')
+ ]
+
+ issue_main.blocked_on_iids = [no_change_iid, bo_remove.issue_id]
+ # By default new blocked_on issues that appear in blocked_on_iids
+ # with no prior rank associated with it are un-ranked and assigned rank 0.
+ # See SortBlockedOn in issue_svc.py.
+ issue_main.blocked_on_ranks = [0, 0]
+ expected_main.blocked_on_iids = [no_change_iid, bo_add.issue_id]
+ expected_main.blocked_on_ranks = [0, 0]
+ expected_amendments[issue_main.issue_id].append(
+ tracker_bizobj.MakeBlockedOnAmendment(
+ [('proj', bo_add.local_id)], [('proj', bo_remove.local_id)],
+ default_project_name='proj'))
+
+ # blocking_issues changes setup.
+ b_add = _Issue('proj', 3)
+ self.services.issue.TestAddIssue(b_add)
+ expected_b_add = copy.deepcopy(b_add)
+ expected_b_add.assume_stale = False
+ expected_b_add.blocked_on_iids = [issue_main.issue_id]
+ expected_b_add.blocked_on_ranks = [0]
+ expected_issues_to_update[expected_b_add.issue_id] = expected_b_add
+ expected_imp_amendments[b_add.issue_id] = [
+ tracker_bizobj.MakeBlockedOnAmendment(
+ [issue_main_ref], [], default_project_name='proj')
+ ]
+
+ b_remove = _Issue('proj', 4)
+ b_remove.blocked_on_iids = [issue_main.issue_id]
+ self.services.issue.TestAddIssue(b_remove)
+ expected_b_remove = copy.deepcopy(b_remove)
+ expected_b_remove.assume_stale = False
+ expected_b_remove.blocked_on_iids = []
+ # Test we can process delta changes and impact changes.
+ delta_b_remove = tracker_pb2.IssueDelta(labels_add=['more_chickens'])
+ expected_b_remove.labels = ['more_chickens']
+ issue_delta_pairs.append((b_remove, delta_b_remove))
+ expected_issues_to_update[expected_b_remove.issue_id] = expected_b_remove
+ expected_imp_amendments[b_remove.issue_id] = [
+ tracker_bizobj.MakeBlockedOnAmendment(
+ [], [issue_main_ref], default_project_name='proj')
+ ]
+ expected_amendments[b_remove.issue_id] = [
+ tracker_bizobj.MakeLabelsAmendment(['more_chickens'], [])
+ ]
+
+ issue_main.blocking_iids = [no_change_iid, b_remove.issue_id]
+ expected_main.blocking_iids = [no_change_iid, b_add.issue_id]
+ expected_amendments[issue_main.issue_id].append(
+ tracker_bizobj.MakeBlockingAmendment(
+ [('proj', b_add.local_id)], [('proj', b_remove.local_id)],
+ default_project_name='proj'))
+
+ # Merged issues changes setup.
+ merge_remove = _Issue('proj', 5)
+ self.services.issue.TestAddIssue(merge_remove)
+ expected_merge_remove = copy.deepcopy(merge_remove)
+ expected_merge_remove.assume_stale = False
+ expected_issues_to_update[
+ expected_merge_remove.issue_id] = expected_merge_remove
+ expected_imp_amendments[merge_remove.issue_id] = [
+ tracker_bizobj.MakeMergedIntoAmendment(
+ [], [issue_main_ref], default_project_name='proj')
+ ]
+
+ merge_add = _Issue('proj', 6)
+ self.services.issue.TestAddIssue(merge_add)
+ expected_merge_add = copy.deepcopy(merge_add)
+ expected_merge_add.assume_stale = False
+ # We are adding 333 and removing 222 in issue_main with delta_main.
+ expected_merge_add.cc_ids = [expected_main.owner_id, 333, 111]
+ expected_merged_from_add[expected_merge_add.issue_id] = [
+ issue_main.issue_id
+ ]
+
+ expected_imp_amendments[merge_add.issue_id] = [
+ tracker_bizobj.MakeCcAmendment(expected_merge_add.cc_ids, []),
+ tracker_bizobj.MakeMergedIntoAmendment(
+ [issue_main_ref], [], default_project_name='proj')
+ ]
+ # We are merging issue_main into merge_add, so issue_main's starrers
+ # should be merged into merge_add's starrers.
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, None, issue_main.issue_id, 111, True)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, None, issue_main.issue_id, 222, True)
+ expected_merge_add.star_count = 2
+ expected_new_starrers[merge_add.issue_id] = [222, 111]
+
+ expected_issues_to_update[expected_merge_add.issue_id] = expected_merge_add
+
+
+ issue_main.merged_into = merge_remove.issue_id
+ expected_main.merged_into = merge_add.issue_id
+ expected_amendments[issue_main.issue_id].append(
+ tracker_bizobj.MakeMergedIntoAmendment(
+ [('proj', merge_add.local_id)], [('proj', merge_remove.local_id)],
+ default_project_name='proj'))
+
+ self.services.issue.TestAddIssue(issue_main)
+ expected_issues_to_update[expected_main.issue_id] = expected_main
+
+
+ # Issues we'll put in delta_main.*_remove fields that aren't in issue_main.
+ # These issues should not show up in issues_to_update.
+ missing_1 = _Issue('proj', 404)
+ expected_missing_1 = copy.deepcopy(missing_1)
+ expected_missing_1.assume_stale = False
+ self.services.issue.TestAddIssue(missing_1)
+ missing_2 = _Issue('proj', 405)
+ self.services.issue.TestAddIssue(missing_2)
+ expected_missing_2 = copy.deepcopy(missing_2)
+ expected_missing_2.assume_stale = False
+
+ delta_main = tracker_pb2.IssueDelta(
+ owner_id=888,
+ cc_ids_remove=[222, 404], cc_ids_add=[333],
+ labels_remove=['remove_me', 'remove_404'], labels_add=['add_me'],
+ merged_into=merge_add.issue_id,
+ blocked_on_add=[bo_add.issue_id],
+ blocked_on_remove=[bo_remove.issue_id, missing_1.issue_id],
+ blocking_add=[b_add.issue_id],
+ blocking_remove=[b_remove.issue_id, missing_2.issue_id])
+ issue_delta_pairs.append((issue_main, delta_main))
+
+ actual_tuple = tracker_helpers.ApplyAllIssueChanges(
+ self.cnxn, issue_delta_pairs, self.services)
+
+ expected_tuple = tracker_helpers._IssueChangesTuple(
+ expected_issues_to_update, expected_merged_from_add,
+ expected_amendments, expected_imp_amendments, expected_old_owners,
+ expected_old_statuses, expected_old_components, expected_new_starrers)
+ self.assertEqual(actual_tuple, expected_tuple)
+
+ self.assertEqual(missing_1, expected_missing_1)
+ self.assertEqual(missing_2, expected_missing_2)
+
+ def testApplyAllIssueChanges_NOOP(self):
+ """Check we can ignore issue-delta pairs that are NOOP."""
+ noop_issue = _Issue('proj', 1)
+ bo_add_noop = _Issue('proj', 2)
+ bo_remove_noop = _Issue('proj', 3)
+
+ noop_issue.owner_id = 111
+ noop_issue.cc_ids = [222]
+ noop_issue.blocked_on_iids = [bo_add_noop.issue_id]
+ bo_add_noop.blocking_iids = [noop_issue.issue_id]
+
+ self.services.issue.TestAddIssue(noop_issue)
+ self.services.issue.TestAddIssue(bo_add_noop)
+ self.services.issue.TestAddIssue(bo_remove_noop)
+ expected_noop_issue = copy.deepcopy(noop_issue)
+ noop_delta = tracker_pb2.IssueDelta(
+ owner_id=noop_issue.owner_id,
+ cc_ids_add=noop_issue.cc_ids, cc_ids_remove=[333],
+ blocked_on_add=noop_issue.blocked_on_iids,
+ blocked_on_remove=[bo_remove_noop.issue_id])
+ issue_delta_pairs = [(noop_issue, noop_delta)]
+
+ actual_tuple = tracker_helpers.ApplyAllIssueChanges(
+ self.cnxn, issue_delta_pairs, self.services)
+ expected_tuple = tracker_helpers._IssueChangesTuple(
+ {}, {}, {}, {}, {}, {}, {}, {})
+ self.assertEqual(actual_tuple, expected_tuple)
+
+ self.assertEqual(noop_issue, expected_noop_issue)
+
+ def testApplyAllIssueChanges_Empty(self):
+ issue_delta_pairs = []
+ actual_tuple = tracker_helpers.ApplyAllIssueChanges(
+ self.cnxn, issue_delta_pairs, self.services)
+ expected_tuple = tracker_helpers._IssueChangesTuple(
+ {}, {}, {}, {}, {}, {}, {}, {})
+ self.assertEqual(actual_tuple, expected_tuple)
+
+ def testUpdateClosedTimestamp(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ config.well_known_statuses.append(
+ tracker_pb2.StatusDef(status='New', means_open=True))
+ config.well_known_statuses.append(
+ tracker_pb2.StatusDef(status='Accepted', means_open=True))
+ config.well_known_statuses.append(
+ tracker_pb2.StatusDef(status='Old', means_open=False))
+ config.well_known_statuses.append(
+ tracker_pb2.StatusDef(status='Closed', means_open=False))
+
+ issue = tracker_pb2.Issue()
+ issue.local_id = 1234
+ issue.status = 'New'
+
+ # ensure the default value is undef
+ self.assertTrue(not issue.closed_timestamp)
+
+ # ensure transitioning to the same and other open states
+ # doesn't set the timestamp
+ issue.status = 'New'
+ tracker_helpers.UpdateClosedTimestamp(config, issue, 'New')
+ self.assertTrue(not issue.closed_timestamp)
+
+ issue.status = 'Accepted'
+ tracker_helpers.UpdateClosedTimestamp(config, issue, 'New')
+ self.assertTrue(not issue.closed_timestamp)
+
+ # ensure transitioning from open to closed sets the timestamp
+ issue.status = 'Closed'
+ tracker_helpers.UpdateClosedTimestamp(config, issue, 'Accepted')
+ self.assertTrue(issue.closed_timestamp)
+
+ # ensure that the timestamp is cleared when transitioning from
+ # closed to open
+ issue.status = 'New'
+ tracker_helpers.UpdateClosedTimestamp(config, issue, 'Closed')
+ self.assertTrue(not issue.closed_timestamp)
+
+ def testGroupUniqueDeltaIssues(self):
+ """We can identify unique IssueDeltas and group Issues by their deltas."""
+ issue_1 = _Issue('proj', 1)
+ delta_1 = tracker_pb2.IssueDelta(cc_ids_add=[111])
+
+ issue_2 = _Issue('proj', 2)
+ delta_2 = tracker_pb2.IssueDelta(cc_ids_add=[111], cc_ids_remove=[222])
+
+ issue_3 = _Issue('proj', 3)
+ delta_3 = tracker_pb2.IssueDelta(cc_ids_add=[111])
+
+ issue_4 = _Issue('proj', 4)
+ delta_4 = tracker_pb2.IssueDelta()
+
+ issue_5 = _Issue('proj', 5)
+ delta_5 = tracker_pb2.IssueDelta()
+
+ issue_delta_pairs = [
+ (issue_1, delta_1), (issue_2, delta_2), (issue_3, delta_3),
+ (issue_4, delta_4), (issue_5, delta_5)
+ ]
+ unique_deltas, issues_for_deltas = tracker_helpers.GroupUniqueDeltaIssues(
+ issue_delta_pairs)
+
+ expected_unique_deltas = [delta_1, delta_2, delta_4]
+ self.assertEqual(unique_deltas, expected_unique_deltas)
+ expected_issues_for_deltas = [
+ [issue_1, issue_3], [issue_2], [issue_4, issue_5]
+ ]
+ self.assertEqual(issues_for_deltas, expected_issues_for_deltas)
+
+ def testEnforceAttachmentQuotaLimits(self):
+ self.services.project.TestAddProject('Circe', project_id=798)
+ issue_a1 = _Issue('Circe', 1, project_id=798)
+ delta_a1 = tracker_pb2.IssueDelta()
+
+ issue_a2 = _Issue('Circe', 2, project_id=798)
+ delta_a2 = tracker_pb2.IssueDelta()
+
+ self.services.project.TestAddProject('Patroclus', project_id=788)
+ issue_b1 = _Issue('Patroclus', 1, project_id=788)
+ delta_b1 = tracker_pb2.IssueDelta()
+
+ issue_delta_pairs = [
+ (issue_a1, delta_a1), (issue_a2, delta_a2), (issue_b1, delta_b1)
+ ]
+
+ upload_1 = framework_helpers.AttachmentUpload(
+ 'dragon', 'OOOOOO\n', 'text/plain')
+ upload_2 = framework_helpers.AttachmentUpload(
+ 'snake', 'ooooo\n', 'text/plain')
+ attachment_uploads = [upload_1, upload_2]
+
+ actual = tracker_helpers._EnforceAttachmentQuotaLimits(
+ self.cnxn, issue_delta_pairs, self.services, attachment_uploads)
+
+ expected = {
+ 798: len(upload_1.contents + upload_2.contents) * 2,
+ 788: len(upload_1.contents + upload_2.contents)
+ }
+ self.assertEqual(actual, expected)
+
+ @mock.patch('tracker.tracker_constants.ISSUE_ATTACHMENTS_QUOTA_HARD', 1)
+ def testEnforceAttachmentQuotaLimits_Exceeded(self):
+ self.services.project.TestAddProject('Circe', project_id=798)
+ issue_a1 = _Issue('Circe', 1, project_id=798)
+ delta_a1 = tracker_pb2.IssueDelta()
+
+ issue_a2 = _Issue('Circe', 2, project_id=798)
+ delta_a2 = tracker_pb2.IssueDelta()
+
+ self.services.project.TestAddProject('Patroclus', project_id=788)
+ issue_b1 = _Issue('Patroclus', 1, project_id=788)
+ delta_b1 = tracker_pb2.IssueDelta()
+
+ issue_delta_pairs = [
+ (issue_a1, delta_a1), (issue_a2, delta_a2), (issue_b1, delta_b1)
+ ]
+
+ upload_1 = framework_helpers.AttachmentUpload(
+ 'dragon', 'OOOOOO\n', 'text/plain')
+ upload_2 = framework_helpers.AttachmentUpload(
+ 'snake', 'ooooo\n', 'text/plain')
+ attachment_uploads = [upload_1, upload_2]
+
+ with self.assertRaisesRegexp(exceptions.OverAttachmentQuota,
+ r'.+ project Patroclus\n.+ project Circe'):
+ tracker_helpers._EnforceAttachmentQuotaLimits(
+ self.cnxn, issue_delta_pairs, self.services, attachment_uploads)
+
+ def testAssertIssueChangesValid_Valid(self):
+ """We can assert when deltas are valid for issues."""
+ impacted_issue = _Issue('chicken', 101)
+ self.services.issue.TestAddIssue(impacted_issue)
+
+ issue_1 = _Issue('chicken', 1)
+ self.services.issue.TestAddIssue(issue_1)
+ delta_1 = tracker_pb2.IssueDelta(
+ merged_into=impacted_issue.issue_id, status='Duplicate')
+ exp_d1 = copy.deepcopy(delta_1)
+
+ issue_2 = _Issue('chicken', 2)
+ self.services.issue.TestAddIssue(issue_2)
+ delta_2 = tracker_pb2.IssueDelta(blocked_on_add=[impacted_issue.issue_id])
+ exp_d2 = copy.deepcopy(delta_2)
+
+ issue_3 = _Issue('chicken', 3)
+ self.services.issue.TestAddIssue(issue_3)
+ delta_3 = tracker_pb2.IssueDelta()
+ exp_d3 = copy.deepcopy(delta_3)
+
+ issue_4 = _Issue('chicken', 4)
+ self.services.issue.TestAddIssue(issue_4)
+ delta_4 = tracker_pb2.IssueDelta(owner_id=self.project_member.user_id)
+ exp_d4 = copy.deepcopy(delta_4)
+
+ issue_5 = _Issue('chicken', 5)
+ self.services.issue.TestAddIssue(issue_5)
+ fv = tracker_bizobj.MakeFieldValue(
+ self.int_fd.field_id, 998, None, None, None, None, False)
+ delta_5 = tracker_pb2.IssueDelta(field_vals_add=[fv])
+ exp_d5 = copy.deepcopy(delta_5)
+
+ issue_6 = _Issue('chicken', 6)
+ self.services.issue.TestAddIssue(issue_6)
+ delta_6 = tracker_pb2.IssueDelta(
+ summary=' ' + 's' * tracker_constants.MAX_SUMMARY_CHARS + ' ')
+ exp_d6 = copy.deepcopy(delta_6)
+
+ issue_7 = _Issue('chicken', 7)
+ self.services.issue.TestAddIssue(issue_7)
+ issue_8 = _Issue('chicken', 8)
+ self.services.issue.TestAddIssue(issue_8)
+
+ # We are fine with duplicate/consistent deltas.
+ delta_7 = tracker_pb2.IssueDelta(blocked_on_add=[issue_8.issue_id])
+ exp_d7 = copy.deepcopy(delta_7)
+ delta_8 = tracker_pb2.IssueDelta(blocking_add=[issue_7.issue_id])
+ exp_d8 = copy.deepcopy(delta_8)
+
+ issue_9 = _Issue('chicken', 9)
+ self.services.issue.TestAddIssue(issue_9)
+ issue_10 = _Issue('chicken', 10)
+ self.services.issue.TestAddIssue(issue_10)
+
+ delta_9 = tracker_pb2.IssueDelta(blocked_on_remove=[issue_10.issue_id])
+ exp_d9 = copy.deepcopy(delta_9)
+ delta_10 = tracker_pb2.IssueDelta(blocking_remove=[issue_9.issue_id])
+ exp_d10 = copy.deepcopy(delta_10)
+
+ issue_11 = _Issue('chicken', 11)
+ user_fd = tracker_bizobj.MakeFieldDef(
+ 123, 789, 'CPU', tracker_pb2.FieldTypes.USER_TYPE, None, '', False,
+ False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.services.config.TestAddFieldDef(user_fd)
+ a_user = self.services.user.TestAddUser('a_user@test.com', 123)
+ delta_11 = tracker_pb2.IssueDelta(
+ cc_ids_add=[222],
+ field_vals_add=[
+ tracker_bizobj.MakeFieldValue(
+ user_fd.field_id, None, None, a_user.user_id, None, None, False)
+ ])
+ exp_d11 = copy.deepcopy(delta_11)
+
+ issue_delta_pairs = [
+ (issue_1, delta_1), (issue_2, delta_2), (issue_3, delta_3),
+ (issue_4, delta_4), (issue_5, delta_5), (issue_6, delta_6),
+ (issue_7, delta_7), (issue_8, delta_8), (issue_9, delta_9),
+ (issue_10, delta_10), (issue_11, delta_11)
+ ]
+ comment = ' ' + 'c' * tracker_constants.MAX_COMMENT_CHARS + ' '
+ tracker_helpers._AssertIssueChangesValid(
+ self.cnxn, issue_delta_pairs, self.services, comment_content=comment)
+
+ # Check we can handle None `comment_content`.
+ tracker_helpers._AssertIssueChangesValid(
+ self.cnxn, issue_delta_pairs, self.services)
+ self.assertEqual(
+ [
+ exp_d1, exp_d2, exp_d3, exp_d4, exp_d5, exp_d6, exp_d7, exp_d8,
+ exp_d9, exp_d10, exp_d11
+ ], [
+ delta_1, delta_2, delta_3, delta_4, delta_5, delta_6, delta_7,
+ delta_8, delta_9, delta_10, delta_11
+ ])
+
+ def testAssertIssueChangesValid_RequiredField(self):
+ """Asserts fields and requried fields.."""
+ issue_1 = _Issue('chicken', 1)
+ self.services.issue.TestAddIssue(issue_1)
+ delta_1 = tracker_pb2.IssueDelta()
+ exp_d1 = copy.deepcopy(delta_1)
+
+ required_fd = tracker_bizobj.MakeFieldDef(
+ 124, 789, 'StrField', tracker_pb2.FieldTypes.STR_TYPE, None, '', True,
+ False, False, None, None, '', False, '', '',
+ tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
+ self.services.config.TestAddFieldDef(required_fd)
+
+ issue_delta_pairs = [(issue_1, delta_1)]
+ comment = 'just a plain comment'
+ tracker_helpers._AssertIssueChangesValid(
+ self.cnxn, issue_delta_pairs, self.services, comment_content=comment)
+
+ # Check we can handle adding a field value when issue is in invalid state.
+ fv = tracker_bizobj.MakeFieldValue(
+ self.int_fd.field_id, 998, None, None, None, None, False)
+ delta_2 = tracker_pb2.IssueDelta(field_vals_add=[fv])
+ exp_d2 = copy.deepcopy(delta_2)
+ tracker_helpers._AssertIssueChangesValid(
+ self.cnxn, issue_delta_pairs, self.services)
+ self.assertEqual([exp_d1, exp_d2], [delta_1, delta_2])
+
+ def testAssertIssueChangesValid_Invalid(self):
+ """We can raise exceptions when deltas are not valid for issues. """
+
+ def getRef(issue):
+ return '%s:%d' % (issue.project_name, issue.local_id)
+
+ issue_delta_pairs = []
+ expected_err_msgs = []
+
+ comment = 'c' * (tracker_constants.MAX_COMMENT_CHARS + 1)
+ expected_err_msgs.append('Comment is too long.')
+
+ issue_1 = _Issue('chicken', 1)
+ self.services.issue.TestAddIssue(issue_1)
+ issue_1_ref = getRef(issue_1)
+
+ delta_1 = tracker_pb2.IssueDelta(
+ merged_into=issue_1.issue_id,
+ blocked_on_add=[issue_1.issue_id],
+ summary='',
+ status='',
+ cc_ids_add=[9876])
+
+ issue_delta_pairs.append((issue_1, delta_1))
+ expected_err_msgs.extend(
+ [
+ ('%s: MERGED type statuses must accompany mergedInto values.') %
+ issue_1_ref,
+ '%s: Cannot merge an issue into itself.' % issue_1_ref,
+ '%s: Cannot block an issue on itself.' % issue_1_ref,
+ 'users/9876: User does not exist.',
+ '%s: Summary required.' % issue_1_ref,
+ '%s: Status is required.' % issue_1_ref
+ ])
+
+ issue_2 = _Issue('chicken', 2)
+ self.services.issue.TestAddIssue(issue_2)
+ issue_2_ref = getRef(issue_2)
+
+ fv = tracker_bizobj.MakeFieldValue(
+ self.int_fd.field_id, 1000, None, None, None, None, False)
+ delta_2 = tracker_pb2.IssueDelta(
+ status='Duplicate',
+ blocking_add=[issue_2.issue_id],
+ summary='s' * (tracker_constants.MAX_SUMMARY_CHARS + 1),
+ owner_id=self.no_project_user.user_id,
+ field_vals_add=[fv])
+ issue_delta_pairs.append((issue_2, delta_2))
+
+ expected_err_msgs.extend(
+ [
+ ('%s: MERGED type statuses must accompany mergedInto values.') %
+ issue_2_ref,
+ '%s: Cannot block an issue on itself.' % issue_2_ref,
+ '%s: Issue owner must be a project member.' % issue_2_ref,
+ '%s: Summary is too long.' % issue_2_ref,
+ '%s: Error for %r: Value must be <= 999.' % (issue_2_ref, fv)
+ ])
+
+ issue_3 = _Issue('chicken', 3)
+ issue_3.status = 'Duplicate'
+ issue_3.merged_into = 78911
+ self.services.issue.TestAddIssue(issue_3)
+ issue_3_ref = getRef(issue_3)
+ delta_3 = tracker_pb2.IssueDelta(
+ status='Available', merged_into_external='b/123')
+ issue_delta_pairs.append((issue_3, delta_3))
+ expected_err_msgs.append(
+ '%s: MERGED type statuses must accompany mergedInto values.' %
+ issue_3_ref)
+
+ with self.assertRaisesRegexp(exceptions.InputException,
+ '\n'.join(expected_err_msgs)):
+ tracker_helpers._AssertIssueChangesValid(
+ self.cnxn, issue_delta_pairs, self.services, comment_content=comment)
+
+ def testAssertIssueChangesValid_ConflictingDeltas(self):
+
+ def getRef(issue):
+ return '%s:%d' % (issue.project_name, issue.local_id)
+
+ expected_err_msgs = []
+ issue_3 = _Issue('chicken', 3)
+ self.services.issue.TestAddIssue(issue_3)
+ issue_3_ref = getRef(issue_3)
+ issue_4 = _Issue('chicken', 4)
+ self.services.issue.TestAddIssue(issue_4)
+ issue_4_ref = getRef(issue_4)
+ issue_5 = _Issue('chicken', 5)
+ self.services.issue.TestAddIssue(issue_5)
+ issue_5_ref = getRef(issue_5)
+ issue_6 = _Issue('chicken', 6)
+ self.services.issue.TestAddIssue(issue_6)
+ issue_6_ref = getRef(issue_6)
+ issue_7 = _Issue('chicken', 7)
+ self.services.issue.TestAddIssue(issue_7)
+ issue_7_ref = getRef(issue_7)
+
+ delta_3 = tracker_pb2.IssueDelta(
+ blocking_add=[issue_4.issue_id],
+ blocked_on_add=[issue_5.issue_id, issue_6.issue_id])
+
+ delta_4 = tracker_pb2.IssueDelta(
+ blocked_on_remove=[issue_3.issue_id], blocking_add=[issue_5.issue_id])
+ expected_err_msgs.append(
+ 'Changes for %s conflict for %s' % (issue_4_ref, issue_3_ref))
+
+ delta_5 = tracker_pb2.IssueDelta(
+ blocking_remove=[issue_3.issue_id],
+ blocked_on_remove=[issue_4.issue_id])
+ expected_err_msgs.append(
+ 'Changes for %s conflict for %s, %s' %
+ (issue_5_ref, issue_3_ref, issue_4_ref))
+
+ delta_6 = tracker_pb2.IssueDelta(blocking_remove=[issue_3.issue_id])
+ expected_err_msgs.append(
+ 'Changes for %s conflict for %s' % (issue_6_ref, issue_3_ref))
+
+ impacted_issue = _Issue('chicken', 11)
+ self.services.issue.TestAddIssue(impacted_issue)
+ impacted_issue_ref = getRef(impacted_issue)
+ delta_7 = tracker_pb2.IssueDelta(
+ blocking_remove=[issue_3.issue_id],
+ blocking_add=[issue_3.issue_id],
+ blocked_on_remove=[impacted_issue.issue_id],
+ blocked_on_add=[impacted_issue.issue_id])
+ expected_err_msgs.append(
+ 'Changes for %s conflict for %s, %s' %
+ (issue_7_ref, issue_3_ref, impacted_issue_ref))
+
+ issue_delta_pairs = [
+ (issue_3, delta_3),
+ (issue_4, delta_4),
+ (issue_5, delta_5),
+ (issue_6, delta_6),
+ (issue_7, delta_7),
+ ]
+
+ with self.assertRaisesRegexp(exceptions.InputException,
+ '\n'.join(expected_err_msgs)):
+ tracker_helpers._AssertIssueChangesValid(
+ self.cnxn, issue_delta_pairs, self.services)
+
+ def testComputeNewCcsFromIssueMerge(self):
+ """We can compute the new ccs to add to a merge-into issue."""
+ target_issue = fake.MakeTestIssue(789, 10, 'Target issue', 'New', 111)
+ source_issue_1 = fake.MakeTestIssue(
+ 789, 11, 'Source issue', 'New', 111) # different restrictions
+ source_issue_2 = fake.MakeTestIssue(
+ 789, 12, 'Source issue', 'New', 222) # same restrictions
+ source_issue_3 = fake.MakeTestIssue(
+ 789, 13, 'Source issue', 'New', 222) # no restrictions
+ source_issue_4 = fake.MakeTestIssue(
+ 789, 14, 'Source issue', 'New', 666) # empty ccs
+ source_issue_5 = fake.MakeTestIssue(
+ 788, 15, 'Source issue', 'New', 666) # different project
+ source_issue_1.cc_ids.append(333)
+ source_issue_2.cc_ids.append(444)
+ source_issue_3.cc_ids.append(555)
+ source_issue_5.cc_ids.append(999)
+
+ target_issue.labels.append('Restrict-View-Chicken')
+ source_issue_1.labels.append('Restrict-View-Cow')
+ source_issue_2.labels.append('Restrict-View-Chicken')
+
+ self.services.issue.TestAddIssue(target_issue)
+ self.services.issue.TestAddIssue(source_issue_1)
+ self.services.issue.TestAddIssue(source_issue_2)
+ self.services.issue.TestAddIssue(source_issue_3)
+ self.services.issue.TestAddIssue(source_issue_4)
+ self.services.issue.TestAddIssue(source_issue_5)
+
+ new_cc_ids = tracker_helpers._ComputeNewCcsFromIssueMerge(
+ target_issue, [source_issue_1, source_issue_2, source_issue_3])
+ self.assertItemsEqual(new_cc_ids, [444, 555, 222])
+
+ def testComputeNewCcsFromIssueMerge_Empty(self):
+ target_issue = fake.MakeTestIssue(789, 10, 'Target issue', 'New', 111)
+ self.services.issue.TestAddIssue(target_issue)
+ new_cc_ids = tracker_helpers._ComputeNewCcsFromIssueMerge(target_issue, [])
+ self.assertItemsEqual(new_cc_ids, [])
+
+ def testEnforceNonMergeStatusDeltas(self):
+ # No updates: user is setting to a non-MERGED status with no
+ # existing merged_into values.
+ issue_1 = _Issue('chicken', 1)
+ self.services.issue.TestAddIssue(issue_1)
+ delta_1 = tracker_pb2.IssueDelta(status='Available')
+ exp_delta_1 = copy.deepcopy(delta_1)
+
+ # No updates: user is setting to a MERGED status. Whether this request
+ # goes through will be handled by _AssertIssueChangesValid().
+ issue_2 = _Issue('chicken', 2)
+ self.services.issue.TestAddIssue(issue_2)
+ delta_2 = tracker_pb2.IssueDelta(status='Duplicate')
+ exp_delta_2 = copy.deepcopy(delta_2)
+
+ # No updates: user is setting to a MERGED status. (This test issue starts
+ # out with a merged_into value but a non-MERGED status. We don't expect
+ # real data to ever be in this state)
+ issue_3 = _Issue('chicken', 3)
+ issue_3.merged_into = 7011
+ self.services.issue.TestAddIssue(issue_3)
+ delta_3 = tracker_pb2.IssueDelta(status='Duplicate')
+ exp_delta_3 = copy.deepcopy(delta_3)
+
+ # No updates: same situation as above.
+ issue_4 = _Issue('chicken', 4)
+ issue_4.merged_into_external = 'b/123'
+ self.services.issue.TestAddIssue(issue_4)
+ delta_4 = tracker_pb2.IssueDelta(status='Duplicate')
+ exp_delta_4 = copy.deepcopy(delta_4)
+
+ # Update delta: user is setting status AWAY from a MERGED status, so we
+ # auto-remove any existing merged_into values.
+ issue_5 = _Issue('chicken', 5)
+ issue_5.merged_into = 7011
+ self.services.issue.TestAddIssue(issue_5)
+ delta_5 = tracker_pb2.IssueDelta(status='Available')
+ exp_delta_5 = copy.deepcopy(delta_5)
+ exp_delta_5.merged_into = 0
+
+ # Update delta: user is setting status AWAY from a MERGED status, so we
+ # auto-remove any existing merged_into values.
+ issue_6 = _Issue('chicken', 6)
+ issue_6.merged_into_external = 'b/123'
+ self.services.issue.TestAddIssue(issue_6)
+ delta_6 = tracker_pb2.IssueDelta(status='Available')
+ exp_delta_6 = copy.deepcopy(delta_6)
+ exp_delta_6.merged_into_external = ''
+
+ # No updates: user is setting to a non-MERGED status while also setting
+ # a merged_into value. This will be rejected down the line by
+ # _AssertIssueChangesValid()
+ issue_7 = _Issue('chicken', 7)
+ issue_7.merged_into = 7011
+ self.services.issue.TestAddIssue(issue_7)
+ delta_7 = tracker_pb2.IssueDelta(
+ merged_into_external='b/123', status='Available')
+ exp_delta_7 = copy.deepcopy(delta_7)
+
+ # No updates: user is setting to a non-MERGED status while also setting
+ # a merged_into value. This will be rejected down the line by
+ # _AssertIssueChangesValid()
+ issue_8 = _Issue('chicken', 8)
+ issue_8.merged_into_external = 'b/123'
+ self.services.issue.TestAddIssue(issue_8)
+ delta_8 = tracker_pb2.IssueDelta(merged_into=8011, status='Available')
+ exp_delta_8 = copy.deepcopy(delta_8)
+
+ pairs = [
+ (issue_1, delta_1), (issue_2, delta_2), (issue_3, delta_3),
+ (issue_4, delta_4), (issue_5, delta_5), (issue_6, delta_6),
+ (issue_7, delta_7), (issue_8, delta_8)
+ ]
+
+ tracker_helpers._EnforceNonMergeStatusDeltas(
+ self.cnxn, pairs, self.services)
+ self.assertEqual(
+ [
+ delta_1, delta_2, delta_3, delta_4, delta_5, delta_6, delta_7,
+ delta_8
+ ], [
+ exp_delta_1, exp_delta_2, exp_delta_3, exp_delta_4, exp_delta_5,
+ exp_delta_6, exp_delta_7, exp_delta_8
+ ])
+
+
+class IssueChangeImpactedIssuesTest(unittest.TestCase):
+ """Tests for the _IssueChangeImpactedIssues class."""
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ issue=fake.IssueService(), issue_star=fake.IssueStarService())
+ self.cnxn = 'fake connection'
+
+ def testComputeAllImpactedIDs(self):
+ tracker = tracker_helpers._IssueChangeImpactedIssues()
+ tracker.blocking_add[78901].append(1)
+ tracker.blocking_remove[78902].append(2)
+ tracker.blocked_on_add[78903].append(1)
+ tracker.blocked_on_remove[78904].append(1)
+ tracker.merged_from_add[78905].append(3)
+ tracker.merged_from_remove[78906].append(3)
+
+ # Repeat a few iids.
+ tracker.blocked_on_remove[78901].append(1)
+ tracker.merged_from_add[78903].append(1)
+
+ actual = tracker.ComputeAllImpactedIIDs()
+ expected = {78901, 78902, 78903, 78904, 78905, 78906}
+ self.assertEqual(actual, expected)
+
+ def testComputeAllImpactedIDs_Empty(self):
+ tracker = tracker_helpers._IssueChangeImpactedIssues()
+ actual = tracker.ComputeAllImpactedIIDs()
+ self.assertEqual(actual, set())
+
+ def testTrackImpactedIssues(self):
+ issue_delta_pairs = []
+
+ issue_1 = _Issue('project', 1)
+ issue_1.merged_into = 78906
+ delta_1 = tracker_pb2.IssueDelta(
+ merged_into=78905,
+ blocked_on_add=[78901, 78902],
+ blocked_on_remove=[78903, 78904],
+ )
+ issue_delta_pairs.append((issue_1, delta_1))
+
+ issue_2 = _Issue('project', 2)
+ issue_2.merged_into = 78905
+ delta_2 = tracker_pb2.IssueDelta(
+ merged_into=78905, # This should be ignored.
+ blocking_add=[78901, 78902],
+ blocking_remove=[78903, 78904],
+ )
+ issue_delta_pairs.append((issue_2, delta_2))
+
+ issue_3 = _Issue('project', 3)
+ issue_3.merged_into = 78902
+ delta_3 = tracker_pb2.IssueDelta(merged_into=78901)
+ issue_delta_pairs.append((issue_3, delta_3))
+
+ issue_4 = _Issue('project', 4)
+ issue_4.merged_into = 78901
+ delta_4 = tracker_pb2.IssueDelta(
+ merged_into=framework_constants.NO_ISSUE_SPECIFIED)
+ issue_delta_pairs.append((issue_4, delta_4))
+
+ impacted_issues = tracker_helpers._IssueChangeImpactedIssues()
+ for issue, delta in issue_delta_pairs:
+ impacted_issues.TrackImpactedIssues(issue, delta)
+
+ self.assertEqual(
+ impacted_issues.blocking_add, {
+ 78901: [issue_1.issue_id],
+ 78902: [issue_1.issue_id]
+ })
+ self.assertEqual(
+ impacted_issues.blocking_remove, {
+ 78903: [issue_1.issue_id],
+ 78904: [issue_1.issue_id]
+ })
+ self.assertEqual(
+ impacted_issues.blocked_on_add, {
+ 78901: [issue_2.issue_id],
+ 78902: [issue_2.issue_id]
+ })
+ self.assertEqual(
+ impacted_issues.blocked_on_remove, {
+ 78903: [issue_2.issue_id],
+ 78904: [issue_2.issue_id]
+ })
+ self.assertEqual(
+ impacted_issues.merged_from_add, {
+ 78901: [issue_3.issue_id],
+ 78905: [issue_1.issue_id],
+ })
+ self.assertEqual(
+ impacted_issues.merged_from_remove, {
+ 78901: [issue_4.issue_id],
+ 78902: [issue_3.issue_id],
+ 78906: [issue_1.issue_id],
+ })
+
+ def testApplyImpactedIssueChanges(self):
+ impacted_tracker = tracker_helpers._IssueChangeImpactedIssues()
+ impacted_issue = _Issue('proj', 1)
+ self.services.issue.TestAddIssue(impacted_issue)
+ impacted_iid = impacted_issue.issue_id
+
+ # Setup.
+ bo_add = _Issue('proj', 2)
+ self.services.issue.TestAddIssue(bo_add)
+ impacted_tracker.blocked_on_add[impacted_iid].append(bo_add.issue_id)
+
+ bo_remove = _Issue('proj', 3)
+ self.services.issue.TestAddIssue(bo_remove)
+ impacted_tracker.blocked_on_remove[impacted_iid].append(
+ bo_remove.issue_id)
+
+ b_add = _Issue('proj', 4)
+ self.services.issue.TestAddIssue(b_add)
+ impacted_tracker.blocking_add[impacted_iid].append(
+ b_add.issue_id)
+
+ b_remove = _Issue('proj', 5)
+ self.services.issue.TestAddIssue(b_remove)
+ impacted_tracker.blocking_remove[impacted_iid].append(
+ b_remove.issue_id)
+
+ m_add = _Issue('proj', 6)
+ m_add.cc_ids = [666, 777]
+ self.services.issue.TestAddIssue(m_add)
+ m_add_no_ccs = _Issue('proj', 7, '', '')
+ self.services.issue.TestAddIssue(m_add_no_ccs)
+ impacted_tracker.merged_from_add[impacted_iid].extend(
+ [m_add.issue_id, m_add_no_ccs.issue_id])
+ # Set up starrers.
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, None, impacted_iid, 111, True)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, None, impacted_iid, 222, True)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, None, m_add.issue_id, 222, True)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, None, m_add.issue_id, 333, True)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, None, m_add.issue_id, 444, True)
+
+ m_remove = _Issue('proj', 8)
+ m_remove.cc_ids = [888]
+ self.services.issue.TestAddIssue(m_remove)
+ impacted_tracker.merged_from_remove[impacted_iid].append(
+ m_remove.issue_id)
+
+
+ impacted_issue.cc_ids = [666]
+ impacted_issue.blocked_on_iids = [78404, bo_remove.issue_id]
+ impacted_issue.blocking_iids = [78405, b_remove.issue_id]
+ expected_issue = copy.deepcopy(impacted_issue)
+
+ # Verify.
+ (actual_amendments,
+ actual_new_starrers) = impacted_tracker.ApplyImpactedIssueChanges(
+ self.cnxn, impacted_issue, self.services)
+ expected_amendments = [
+ tracker_bizobj.MakeBlockedOnAmendment(
+ [('proj', bo_add.local_id)],
+ [('proj', bo_remove.local_id)], default_project_name='proj'),
+ tracker_bizobj.MakeBlockingAmendment(
+ [('proj', b_add.local_id)],
+ [('proj', b_remove.local_id)], default_project_name='proj'),
+ tracker_bizobj.MakeCcAmendment([777], []),
+ tracker_bizobj.MakeMergedIntoAmendment(
+ [('proj', m_add.local_id), ('proj', m_add_no_ccs.local_id)],
+ [('proj', m_remove.local_id)], default_project_name='proj')
+ ]
+ self.assertEqual(actual_amendments, expected_amendments)
+ self.assertItemsEqual(actual_new_starrers, [333, 444])
+
+ expected_issue.cc_ids.append(777)
+ expected_issue.blocked_on_iids = [78404, bo_add.issue_id]
+ # By default new blocked_on issues that appear in blocked_on_iids
+ # with no prior rank associated with it are un-ranked and assigned rank 0.
+ # See SortBlockedOn in issue_svc.py.
+ expected_issue.blocked_on_ranks = [0, 0]
+ expected_issue.blocking_iids = [78405, b_add.issue_id]
+ expected_issue.star_count = 4
+ self.assertEqual(impacted_issue, expected_issue)
+
+ def testApplyImpactedIssueChanges_Empty(self):
+ impacted_tracker = tracker_helpers._IssueChangeImpactedIssues()
+ impacted_issue = _Issue('proj', 1)
+ expected_issue = copy.deepcopy(impacted_issue)
+
+ (actual_amendments,
+ actual_new_starrers) = impacted_tracker.ApplyImpactedIssueChanges(
+ self.cnxn, impacted_issue, self.services)
+
+ expected_amendments = []
+ self.assertEqual(actual_amendments, expected_amendments)
+ expected_new_starrers = []
+ self.assertEqual(actual_new_starrers, expected_new_starrers)
+ self.assertEqual(impacted_issue, expected_issue)
+
+ def testApplyImpactedIssueChanges_PartiallyEmptyMergedFrom(self):
+ """We can process merged_from changes when one of the lists is empty."""
+ impacted_tracker = tracker_helpers._IssueChangeImpactedIssues()
+ impacted_issue = _Issue('proj', 1)
+ impacted_iid = impacted_issue.issue_id
+ expected_issue = copy.deepcopy(impacted_issue)
+
+ m_add = _Issue('proj', 2)
+ self.services.issue.TestAddIssue(m_add)
+ impacted_tracker.merged_from_add[impacted_iid].append(
+ m_add.issue_id)
+ # We're leaving impacted_tracker.merged_from_remove empty.
+
+ (actual_amendments,
+ actual_new_starrers) = impacted_tracker.ApplyImpactedIssueChanges(
+ self.cnxn, impacted_issue, self.services)
+
+ expected_amendments = [tracker_bizobj.MakeMergedIntoAmendment(
+ [('proj', m_add.local_id)], [], default_project_name='proj')]
+ self.assertEqual(actual_amendments, expected_amendments)
+ expected_new_starrers = []
+ self.assertEqual(actual_new_starrers, expected_new_starrers)
+ self.assertEqual(impacted_issue, expected_issue)
+
+
+class AssertUsersExistTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.services = service_manager.Services(user=fake.UserService())
+ for user_id in [1, 1001, 1002, 1003, 2001, 2002, 3002]:
+ self.services.user.TestAddUser('test%d' % user_id, user_id, add_user=True)
+
+ def test_AssertUsersExist_Passes(self):
+ existing = [1, 1001, 1002, 1003, 2001, 2002, 3002]
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ tracker_helpers.AssertUsersExist(
+ self.cnxn, self.services, existing, err_agg)
+
+ def test_AssertUsersExist_Empty(self):
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ tracker_helpers.AssertUsersExist(
+ self.cnxn, self.services, [], err_agg)
+
+ def test_AssertUsersExist(self):
+ dne_users = [2, 3]
+ existing = [1, 1001, 1002, 1003, 2001, 2002, 3002]
+ all_users = existing + dne_users
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'users/2: User does not exist.\nusers/3: User does not exist.'):
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ tracker_helpers.AssertUsersExist(
+ self.cnxn, self.services, all_users, err_agg)
diff --git a/tracker/test/tracker_views_test.py b/tracker/test/tracker_views_test.py
new file mode 100644
index 0000000..797b079
--- /dev/null
+++ b/tracker/test/tracker_views_test.py
@@ -0,0 +1,787 @@
+# Copyright 2016 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
+
+"""Unittest for issue tracker views."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import unittest
+
+import mox
+
+from google.appengine.api import app_identity
+import ezt
+
+from framework import framework_views
+from framework import gcs_helpers
+from framework import template_helpers
+from framework import urls
+from proto import project_pb2
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import attachment_helpers
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+from tracker import tracker_helpers
+from tracker import tracker_views
+
+
+def _Issue(project_name, local_id, summary, status):
+ issue = tracker_pb2.Issue()
+ issue.project_name = project_name
+ issue.local_id = local_id
+ issue.issue_id = 100000 + local_id
+ issue.summary = summary
+ issue.status = status
+ return issue
+
+
+def _MakeConfig():
+ config = tracker_pb2.ProjectIssueConfig()
+ config.well_known_labels = [
+ tracker_pb2.LabelDef(
+ label='Priority-High', label_docstring='Must be resolved'),
+ tracker_pb2.LabelDef(
+ label='Priority-Low', label_docstring='Can be slipped'),
+ ]
+ config.well_known_statuses.append(tracker_pb2.StatusDef(
+ status='New', means_open=True))
+ config.well_known_statuses.append(tracker_pb2.StatusDef(
+ status='Old', means_open=False))
+ return config
+
+
+class IssueViewTest(unittest.TestCase):
+
+ def setUp(self):
+ self.issue1 = _Issue('proj', 1, 'not too long summary', 'New')
+ self.issue2 = _Issue('proj', 2, 'sum 2', '')
+ self.issue3 = _Issue('proj', 3, 'sum 3', '')
+ self.issue4 = _Issue('proj', 4, 'sum 4', '')
+
+ self.issue1.reporter_id = 1002
+ self.issue1.owner_id = 2002
+ self.issue1.labels.extend(['A', 'B'])
+ self.issue1.derived_labels.extend(['C', 'D'])
+
+ self.issue2.reporter_id = 2002
+ self.issue2.labels.extend(['foo', 'bar'])
+ self.issue2.blocked_on_iids.extend(
+ [self.issue1.issue_id, self.issue3.issue_id])
+ self.issue2.blocking_iids.extend(
+ [self.issue1.issue_id, self.issue4.issue_id])
+ dref = tracker_pb2.DanglingIssueRef()
+ dref.project = 'codesite'
+ dref.issue_id = 5001
+ self.issue2.dangling_blocking_refs.append(dref)
+
+ self.issue3.reporter_id = 3002
+ self.issue3.labels.extend(['Hot'])
+
+ self.issue4.reporter_id = 3002
+ self.issue4.labels.extend(['Foo', 'Bar'])
+
+ self.restricted = _Issue('proj', 7, 'summary 7', '')
+ self.restricted.labels.extend([
+ 'Restrict-View-Commit', 'Restrict-View-MyCustomPerm'])
+ self.restricted.derived_labels.extend([
+ 'Restrict-AddIssueComment-Commit', 'Restrict-EditIssue-Commit',
+ 'Restrict-Action-NeededPerm'])
+
+ self.users_by_id = {
+ 0: 'user 0',
+ 1002: 'user 1002',
+ 2002: 'user 2002',
+ 3002: 'user 3002',
+ 4002: 'user 4002',
+ }
+
+ def CheckSimpleIssueView(self, config):
+ view1 = tracker_views.IssueView(
+ self.issue1, self.users_by_id, config)
+ self.assertEqual('not too long summary', view1.summary)
+ self.assertEqual('New', view1.status.name)
+ self.assertEqual('user 2002', view1.owner)
+ self.assertEqual('A', view1.labels[0].name)
+ self.assertEqual('B', view1.labels[1].name)
+ self.assertEqual('C', view1.derived_labels[0].name)
+ self.assertEqual('D', view1.derived_labels[1].name)
+ self.assertEqual([], view1.blocked_on)
+ self.assertEqual([], view1.blocking)
+ detail_url = '/p/%s%s?id=%d' % (
+ self.issue1.project_name, urls.ISSUE_DETAIL,
+ self.issue1.local_id)
+ self.assertEqual(detail_url, view1.detail_relative_url)
+ return view1
+
+ def testSimpleIssueView(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ view1 = self.CheckSimpleIssueView(config)
+ self.assertEqual('', view1.status.docstring)
+
+ config.well_known_statuses.append(tracker_pb2.StatusDef(
+ status='New', status_docstring='Issue has not had review yet'))
+ view1 = self.CheckSimpleIssueView(config)
+ self.assertEqual('Issue has not had review yet',
+ view1.status.docstring)
+ self.assertIsNone(view1.restrictions.has_restrictions)
+ self.assertEqual('', view1.restrictions.view)
+ self.assertEqual('', view1.restrictions.add_comment)
+ self.assertEqual('', view1.restrictions.edit)
+
+ def testIsOpen(self):
+ config = _MakeConfig()
+ view1 = tracker_views.IssueView(
+ self.issue1, self.users_by_id, config)
+ self.assertEqual(ezt.boolean(True), view1.is_open)
+
+ self.issue1.status = 'Old'
+ view1 = tracker_views.IssueView(
+ self.issue1, self.users_by_id, config)
+ self.assertEqual(ezt.boolean(False), view1.is_open)
+
+ def testIssueViewWithRestrictions(self):
+ view = tracker_views.IssueView(
+ self.restricted, self.users_by_id, _MakeConfig())
+ self.assertTrue(view.restrictions.has_restrictions)
+ self.assertEqual('Commit and MyCustomPerm', view.restrictions.view)
+ self.assertEqual('Commit', view.restrictions.add_comment)
+ self.assertEqual('Commit', view.restrictions.edit)
+ self.assertEqual(['Restrict-Action-NeededPerm'], view.restrictions.other)
+ self.assertEqual('Restrict-View-Commit', view.labels[0].name)
+ self.assertTrue(view.labels[0].is_restrict)
+
+
+class RestrictionsViewTest(unittest.TestCase):
+ pass # TODO(jrobbins): write tests
+
+
+class AttachmentViewTest(unittest.TestCase):
+
+ def setUp(self):
+ self.orig_sign_attachment_id = attachment_helpers.SignAttachmentID
+ attachment_helpers.SignAttachmentID = (
+ lambda aid: 'signed_%d' % aid)
+
+ def tearDown(self):
+ attachment_helpers.SignAttachmentID = self.orig_sign_attachment_id
+
+ def MakeViewAndVerifyFields(
+ self, size, name, mimetype, expected_size_str, expect_viewable):
+ attach_pb = tracker_pb2.Attachment()
+ attach_pb.filesize = size
+ attach_pb.attachment_id = 12345
+ attach_pb.filename = name
+ attach_pb.mimetype = mimetype
+
+ view = tracker_views.AttachmentView(attach_pb, 'proj')
+ self.assertEqual('/images/paperclip.png', view.iconurl)
+ self.assertEqual(expected_size_str, view.filesizestr)
+ dl = 'attachment?aid=12345&signed_aid=signed_12345'
+ self.assertEqual(dl, view.downloadurl)
+ if expect_viewable:
+ self.assertEqual(dl + '&inline=1', view.url)
+ self.assertEqual(dl + '&inline=1&thumb=1', view.thumbnail_url)
+ else:
+ self.assertEqual(None, view.url)
+ self.assertEqual(None, view.thumbnail_url)
+
+ def testNonImage(self):
+ self.MakeViewAndVerifyFields(
+ 123, 'file.ext', 'funky/bits', '123 bytes', False)
+
+ def testViewableImage(self):
+ self.MakeViewAndVerifyFields(
+ 123, 'logo.gif', 'image/gif', '123 bytes', True)
+
+ self.MakeViewAndVerifyFields(
+ 123, 'screenshot.jpg', 'image/jpeg', '123 bytes', True)
+
+ def testHugeImage(self):
+ self.MakeViewAndVerifyFields(
+ 18 * 1024 * 1024, 'panorama.png', 'image/jpeg', '18.0 MB', False)
+
+ def testViewableText(self):
+ name = 'hello.c'
+ attach_pb = tracker_pb2.Attachment()
+ attach_pb.filesize = 1234
+ attach_pb.attachment_id = 12345
+ attach_pb.filename = name
+ attach_pb.mimetype = 'text/plain'
+ view = tracker_views.AttachmentView(attach_pb, 'proj')
+
+ view_url = '/p/proj/issues/attachmentText?aid=12345'
+ self.assertEqual(view_url, view.url)
+
+
+class LogoViewTest(unittest.TestCase):
+
+ def setUp(self):
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testProjectWithLogo(self):
+ bucket_name = 'testbucket'
+ logo_gcs_id = '123'
+ logo_file_name = 'logo.png'
+ project_pb = project_pb2.MakeProject(
+ 'testProject', logo_gcs_id=logo_gcs_id, logo_file_name=logo_file_name)
+
+ self.mox.StubOutWithMock(app_identity, 'get_default_gcs_bucket_name')
+ app_identity.get_default_gcs_bucket_name().AndReturn(bucket_name)
+
+ self.mox.StubOutWithMock(gcs_helpers, 'SignUrl')
+ gcs_helpers.SignUrl(bucket_name,
+ logo_gcs_id + '-thumbnail').AndReturn('signed/url')
+ gcs_helpers.SignUrl(bucket_name, logo_gcs_id).AndReturn('signed/url')
+
+ self.mox.ReplayAll()
+
+ view = tracker_views.LogoView(project_pb)
+ self.mox.VerifyAll()
+ self.assertEqual('logo.png', view.filename)
+ self.assertEqual('image/png', view.mimetype)
+ self.assertEqual('signed/url', view.thumbnail_url)
+ self.assertEqual(
+ 'signed/url&response-content-displacement=attachment%3B'
+ '+filename%3Dlogo.png', view.viewurl)
+
+ def testProjectWithNoLogo(self):
+ project_pb = project_pb2.MakeProject('testProject')
+ view = tracker_views.LogoView(project_pb)
+ self.assertEqual('', view.thumbnail_url)
+ self.assertEqual('', view.viewurl)
+
+
+class AmendmentViewTest(unittest.TestCase):
+ pass # TODO(jrobbins): write tests
+
+
+class ComponentDefViewTest(unittest.TestCase):
+ def setUp(self):
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ config=fake.ConfigService())
+ self.services.user.TestAddUser('admin@example.com', 111)
+ self.services.user.TestAddUser('cc@example.com', 222)
+ self.users_by_id = framework_views.MakeAllUserViews(
+ 'cnxn', self.services.user, [111, 222])
+ self.services.config.TestAddLabelsDict({'Hot': 1, 'Cold': 2})
+ self.cd = tracker_bizobj.MakeComponentDef(
+ 10, 789, 'UI', 'User interface', False,
+ [111], [222], 0, 111, label_ids=[1, 2])
+
+ def testRootComponent(self):
+ view = tracker_views.ComponentDefView(
+ 'cnxn', self.services, self.cd, self.users_by_id)
+ self.assertEqual('', view.parent_path)
+ self.assertEqual('UI', view.leaf_name)
+ self.assertEqual('User interface', view.docstring_short)
+ self.assertEqual('admin@example.com', view.admins[0].email)
+ self.assertEqual(['Hot', 'Cold'], view.labels)
+ self.assertEqual('all toplevel active ', view.classes)
+
+ def testNestedComponent(self):
+ self.cd.path = 'UI>Dialogs>Print'
+ view = tracker_views.ComponentDefView(
+ 'cnxn', self.services, self.cd, self.users_by_id)
+ self.assertEqual('UI>Dialogs', view.parent_path)
+ self.assertEqual('Print', view.leaf_name)
+ self.assertEqual('User interface', view.docstring_short)
+ self.assertEqual('admin@example.com', view.admins[0].email)
+ self.assertEqual(['Hot', 'Cold'], view.labels)
+ self.assertEqual('all active ', view.classes)
+
+
+class ComponentValueTest(unittest.TestCase):
+ pass # TODO(jrobbins): write tests
+
+
+class FieldValueViewTest(unittest.TestCase):
+
+ def setUp(self):
+ self.config = tracker_pb2.ProjectIssueConfig()
+ self.estdays_fd = tracker_bizobj.MakeFieldDef(
+ 1, 789, 'EstDays', tracker_pb2.FieldTypes.INT_TYPE, None,
+ None, False, False, False, 3, 99, None, False, None, None,
+ None, 'no_action', 'descriptive docstring', False, approval_id=None,
+ is_phase_field=False)
+ self.designdoc_fd = tracker_bizobj.MakeFieldDef(
+ 2, 789, 'DesignDoc', tracker_pb2.FieldTypes.STR_TYPE, 'Enhancement',
+ None, False, False, False, None, None, None, False, None, None,
+ None, 'no_action', 'descriptive docstring', False, approval_id=None,
+ is_phase_field=False)
+ self.mtarget_fd = tracker_bizobj.MakeFieldDef(
+ 3, 789, 'M-Target', tracker_pb2.FieldTypes.INT_TYPE, 'Enhancement',
+ None, False, False, False, None, None, None, False, None, None,
+ None, 'no_action', 'doc doc', False, approval_id=None,
+ is_phase_field=True)
+ self.config.field_defs = [self.estdays_fd, self.designdoc_fd]
+
+ def testNoValues(self):
+ """We can create a FieldValueView with no values."""
+ values = []
+ derived_values = []
+ estdays_fvv = tracker_views.FieldValueView(
+ self.estdays_fd, self.config, values, derived_values, ['defect'],
+ phase_name='Gate')
+ self.assertEqual('EstDays', estdays_fvv.field_def.field_name)
+ self.assertEqual(3, estdays_fvv.field_def.min_value)
+ self.assertEqual(99, estdays_fvv.field_def.max_value)
+ self.assertEqual([], estdays_fvv.values)
+ self.assertEqual([], estdays_fvv.derived_values)
+
+ def testSomeValues(self):
+ """We can create a FieldValueView with some values."""
+ values = [template_helpers.EZTItem(val=12, docstring=None, idx=0)]
+ derived_values = [template_helpers.EZTItem(val=88, docstring=None, idx=0)]
+ estdays_fvv = tracker_views.FieldValueView(
+ self.estdays_fd, self.config, values, derived_values, ['defect'])
+ self.assertEqual(self.estdays_fd, estdays_fvv.field_def.field_def)
+ self.assertTrue(estdays_fvv.is_editable)
+ self.assertEqual(values, estdays_fvv.values)
+ self.assertEqual(derived_values, estdays_fvv.derived_values)
+ self.assertEqual('', estdays_fvv.phase_name)
+ self.assertEqual(ezt.boolean(False), estdays_fvv.field_def.is_phase_field)
+
+ def testApplicability(self):
+ """We know whether a field should show an editing widget."""
+ # Not the right type and has no values.
+ designdoc_fvv = tracker_views.FieldValueView(
+ self.designdoc_fd, self.config, [], [], ['defect'])
+ self.assertFalse(designdoc_fvv.applicable)
+ self.assertEqual('', designdoc_fvv.phase_name)
+ self.assertEqual(ezt.boolean(False), designdoc_fvv.field_def.is_phase_field)
+
+ # Has a value.
+ designdoc_fvv = tracker_views.FieldValueView(
+ self.designdoc_fd, self.config, ['fake value item'], [], ['defect'])
+ self.assertTrue(designdoc_fvv.applicable)
+
+ # Derived values don't cause editing fields to display.
+ designdoc_fvv = tracker_views.FieldValueView(
+ self.designdoc_fd, self.config, [], ['fake value item'], ['defect'])
+ self.assertFalse(designdoc_fvv.applicable)
+
+ # Applicable to this type of issue.
+ designdoc_fvv = tracker_views.FieldValueView(
+ self.designdoc_fd, self.config, [], [], ['enhancement'])
+ self.assertTrue(designdoc_fvv.applicable)
+
+ # Applicable to some issues in a bulk edit.
+ designdoc_fvv = tracker_views.FieldValueView(
+ self.designdoc_fd, self.config, [], [],
+ ['defect', 'task', 'enhancement'])
+ self.assertTrue(designdoc_fvv.applicable)
+
+ # Applicable to all issues.
+ estdays_fvv = tracker_views.FieldValueView(
+ self.estdays_fd, self.config, [], [], ['enhancement'])
+ self.assertTrue(estdays_fvv.applicable)
+
+ # Explicitly set to be applicable when showing bounce values.
+ designdoc_fvv = tracker_views.FieldValueView(
+ self.designdoc_fd, self.config, [], [], ['defect'],
+ applicable=True)
+ self.assertTrue(designdoc_fvv.applicable)
+
+ def testDisplay(self):
+ """We know when a value (or --) should be shown in the metadata column."""
+ # Not the right type and has no values.
+ designdoc_fvv = tracker_views.FieldValueView(
+ self.designdoc_fd, self.config, [], [], ['defect'])
+ self.assertFalse(designdoc_fvv.display)
+
+ # Has a value.
+ designdoc_fvv = tracker_views.FieldValueView(
+ self.designdoc_fd, self.config, ['fake value item'], [], ['defect'])
+ self.assertTrue(designdoc_fvv.display)
+
+ # Has a derived value.
+ designdoc_fvv = tracker_views.FieldValueView(
+ self.designdoc_fd, self.config, [], ['fake value item'], ['defect'])
+ self.assertTrue(designdoc_fvv.display)
+
+ # Applicable to this type of issue, it will show "--".
+ designdoc_fvv = tracker_views.FieldValueView(
+ self.designdoc_fd, self.config, [], [], ['enhancement'])
+ self.assertTrue(designdoc_fvv.display)
+
+ # Applicable to all issues, it will show "--".
+ estdays_fvv = tracker_views.FieldValueView(
+ self.estdays_fd, self.config, [], [], ['enhancement'])
+ self.assertTrue(estdays_fvv.display)
+
+ def testPhaseField(self):
+ mtarget_fvv = tracker_views.FieldValueView(
+ self.mtarget_fd, self.config, [], [], [], phase_name='Stage')
+ self.assertEqual('Stage', mtarget_fvv.phase_name)
+ self.assertEqual(ezt.boolean(True), mtarget_fvv.field_def.is_phase_field)
+
+
+class FVVFunctionsTest(unittest.TestCase):
+
+ def setUp(self):
+ self.config = tracker_pb2.ProjectIssueConfig()
+ self.estdays_fd = tracker_bizobj.MakeFieldDef(
+ 1, 789, 'EstDays', tracker_pb2.FieldTypes.INT_TYPE, None,
+ None, False, False, False, 3, 99, None, False, None, None,
+ None, 'no_action', 'descriptive docstring', False, None, False)
+ self.os_fd = tracker_bizobj.MakeFieldDef(
+ 2, 789, 'OS', tracker_pb2.FieldTypes.ENUM_TYPE,
+ 'Enhancement', None, False, False, False, None, None, None,
+ False, None, None, None, 'no_action', 'descriptive docstring',
+ False, None, False)
+ self.milestone_fd = tracker_bizobj.MakeFieldDef(
+ 3, 789, 'Launch-Milestone', tracker_pb2.FieldTypes.ENUM_TYPE,
+ 'Enhancement', None, False, False, False, None, None, None,
+ False, None, None, None, 'no_action', 'descriptive docstring',
+ False, None, False)
+ self.config.field_defs = [self.estdays_fd, self.os_fd, self.milestone_fd]
+ self.config.well_known_labels = [
+ tracker_pb2.LabelDef(
+ label='Priority-High', label_docstring='Must be resolved'),
+ tracker_pb2.LabelDef(
+ label='Priority-Low', label_docstring='Can be slipped'),
+ ]
+
+ def testPrecomputeInfoForValueViews_NoValues(self):
+ """We can precompute info needed for an issue with no fields or labels."""
+ labels = []
+ derived_labels = []
+ field_values = []
+ phases = []
+ precomp_view_info = tracker_views._PrecomputeInfoForValueViews(
+ labels, derived_labels, field_values, self.config, phases)
+ (labels_by_prefix, der_labels_by_prefix, field_values_by_id,
+ label_docs, phases_by_name) = precomp_view_info
+ self.assertEqual({}, labels_by_prefix)
+ self.assertEqual({}, der_labels_by_prefix)
+ self.assertEqual({}, field_values_by_id)
+ self.assertEqual(
+ {'priority-high': 'Must be resolved',
+ 'priority-low': 'Can be slipped'},
+ label_docs)
+ self.assertEqual({}, phases_by_name)
+
+ def testPrecomputeInfoForValueViews_SomeValues(self):
+ """We can precompute info needed for an issue with fields and labels."""
+ labels = ['Priority-Low', 'GoodFirstBug', 'Feature-UI', 'Feature-Installer',
+ 'Launch-Milestone-66']
+ derived_labels = ['OS-Windows', 'OS-Linux']
+ field_values = [
+ tracker_bizobj.MakeFieldValue(1, 5, None, None, None, None, False),
+ ]
+ phase_1 = tracker_pb2.Phase(phase_id=1, name='Stable')
+ phase_2 = tracker_pb2.Phase(phase_id=2, name='Beta')
+ phase_3 = tracker_pb2.Phase(phase_id=3, name='stable')
+ precomp_view_info = tracker_views._PrecomputeInfoForValueViews(
+ labels, derived_labels, field_values, self.config,
+ phases=[phase_1, phase_2, phase_3])
+ (labels_by_prefix, der_labels_by_prefix, field_values_by_id,
+ _label_docs, phases_by_name) = precomp_view_info
+ self.assertEqual(
+ {'priority': ['Low'],
+ 'feature': ['UI', 'Installer'],
+ 'launch-milestone': ['66']},
+ labels_by_prefix)
+ self.assertEqual(
+ {'os': ['Windows', 'Linux']},
+ der_labels_by_prefix)
+ self.assertEqual(
+ {1: field_values},
+ field_values_by_id)
+ self.assertEqual(
+ {'stable': [phase_1, phase_3],
+ 'beta': [phase_2]},
+ phases_by_name)
+
+ def testMakeAllFieldValueViews(self):
+ labels = ['Priority-Low', 'GoodFirstBug', 'Feature-UI', 'Feature-Installer',
+ 'Launch-Milestone-66']
+ derived_labels = ['OS-Windows', 'OS-Linux']
+ self.config.field_defs.append(tracker_bizobj.MakeFieldDef(
+ 4, 789, 'UIMocks', tracker_pb2.FieldTypes.URL_TYPE,
+ 'Enhancement', None, False, False, False, None, None, None,
+ False, None, None, None, 'no_action', 'descriptive docstring',
+ False, approval_id=23, is_phase_field=False))
+ self.config.field_defs.append(tracker_bizobj.MakeFieldDef(
+ 5, 789, 'LegalFAQs', tracker_pb2.FieldTypes.URL_TYPE,
+ 'Enhancement', None, False, False, False, None, None, None,
+ False, None, None, None, 'no_action', 'descriptive docstring',
+ False, approval_id=26, is_phase_field=False))
+ self.config.field_defs.append(tracker_bizobj.MakeFieldDef(
+ 23, 789, 'Legal', tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ 'Enhancement', None, False, False, False, None, None, None,
+ False, None, None, None, 'no_action', 'descriptive docstring',
+ False, approval_id=None, is_phase_field=False))
+ self.config.field_defs.append(tracker_bizobj.MakeFieldDef(
+ 26, 789, 'UI', tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ 'Enhancement', None, False, False, False, None, None, None,
+ False, None, None, None, 'no_action', 'descriptive docstring',
+ False, approval_id=None, is_phase_field=False))
+ self.config.field_defs.append(tracker_bizobj.MakeFieldDef(
+ 27, 789, 'M-Target', tracker_pb2.FieldTypes.INT_TYPE,
+ 'Enhancement', None, False, False, False, None, None, None,
+ False, None, None, None, 'no_action', 'descriptive docstring',
+ False, approval_id=None, is_phase_field=True))
+ field_values = [
+ tracker_bizobj.MakeFieldValue(1, 5, None, None, None, None, False),
+ tracker_bizobj.MakeFieldValue(
+ 27, 74, None, None, None, None, False, phase_id=3),
+ # phase_id=4 does not belong to any of the phases given below.
+ # this field value should not show up in the views.
+ tracker_bizobj.MakeFieldValue(
+ 27, 79, None, None, None, None, False, phase_id=4),
+ ]
+ users_by_id = {}
+ phase_1 = tracker_pb2.Phase(phase_id=1, name='Stable')
+ phase_2 = tracker_pb2.Phase(phase_id=2, name='Beta')
+ phase_3 = tracker_pb2.Phase(phase_id=3, name='stable')
+ fvvs = tracker_views.MakeAllFieldValueViews(
+ self.config, labels, derived_labels, field_values, users_by_id,
+ parent_approval_ids=[23], phases=[phase_1, phase_2, phase_3])
+ self.assertEqual(9, len(fvvs))
+ # Values are sorted by (applicable_type, field_name).
+ logging.info([fv.field_name for fv in fvvs])
+ (estdays_fvv, launch_milestone_fvv, legal_fvv, legal_faq_fvv,
+ beta_mtarget_fvv, stable_mtarget_fvv, os_fvv, ui_fvv, ui_mocks_fvv) = fvvs
+ self.assertEqual('EstDays', estdays_fvv.field_name)
+ self.assertEqual(1, len(estdays_fvv.values))
+ self.assertEqual(0, len(estdays_fvv.derived_values))
+ self.assertEqual('Launch-Milestone', launch_milestone_fvv.field_name)
+ self.assertEqual(1, len(launch_milestone_fvv.values))
+ self.assertEqual(0, len(launch_milestone_fvv.derived_values))
+ self.assertEqual('OS', os_fvv.field_name)
+ self.assertEqual(0, len(os_fvv.values))
+ self.assertEqual(2, len(os_fvv.derived_values))
+ self.assertEqual(ui_mocks_fvv.field_name, 'UIMocks')
+ self.assertEqual(ui_mocks_fvv.phase_name, '')
+ self.assertTrue(ui_mocks_fvv.applicable)
+ self.assertEqual(legal_faq_fvv.field_name, 'LegalFAQs')
+ self.assertFalse(legal_faq_fvv.applicable)
+ self.assertFalse(legal_fvv.applicable)
+ self.assertFalse(ui_fvv.applicable)
+ self.assertEqual('M-Target', stable_mtarget_fvv.field_name)
+ self.assertEqual('stable', stable_mtarget_fvv.phase_name)
+ self.assertEqual(1, len(stable_mtarget_fvv.values))
+ self.assertEqual(74, stable_mtarget_fvv.values[0].val)
+ self.assertEqual(0, len(stable_mtarget_fvv.derived_values))
+ self.assertEqual('M-Target', beta_mtarget_fvv.field_name)
+ self.assertEqual('beta', beta_mtarget_fvv.phase_name)
+ self.assertEqual(0, len(beta_mtarget_fvv.values))
+ self.assertEqual(0, len(beta_mtarget_fvv.values))
+
+ def testMakeFieldValueView(self):
+ pass # Covered by testMakeAllFieldValueViews()
+
+ def testMakeFieldValueItemsTest(self):
+ pass # Covered by testMakeAllFieldValueViews()
+
+ def testMakeBounceFieldValueViews(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ fd = tracker_pb2.FieldDef(
+ field_id=3, field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ applicable_type='', field_name='EstDays')
+ phase_fd = tracker_pb2.FieldDef(
+ field_id=4, field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ applicable_type='', field_name='Gump')
+ config.field_defs = [fd,
+ phase_fd,
+ tracker_pb2.FieldDef(
+ field_id=5, field_type=tracker_pb2.FieldTypes.STR_TYPE)
+ ]
+ parsed_fvs = {3: [455]}
+ parsed_phase_fvs = {
+ 4: {'stable': [73, 74], 'beta': [8], 'beta-exp': [75]},
+ }
+ fvs = tracker_views.MakeBounceFieldValueViews(
+ parsed_fvs, parsed_phase_fvs, config)
+
+ self.assertEqual(len(fvs), 4)
+
+ estdays_ezt_fv = template_helpers.EZTItem(val=455, docstring='', idx=0)
+ expected = tracker_views.FieldValueView(
+ fd, config, [estdays_ezt_fv], [], [])
+ self.assertEqual(fvs[0].field_name, expected.field_name)
+ self.assertEqual(fvs[0].values[0].val, expected.values[0].val)
+ self.assertEqual(fvs[0].values[0].idx, expected.values[0].idx)
+ self.assertTrue(fvs[0].applicable)
+
+ self.assertEqual(fvs[1].field_name, phase_fd.field_name)
+ self.assertEqual(fvs[2].field_name, phase_fd.field_name)
+ self.assertEqual(fvs[3].field_name, phase_fd.field_name)
+
+ fd.approval_id = 23
+ config.field_defs = [fd,
+ tracker_pb2.FieldDef(
+ field_id=23, field_name='Legal',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE)]
+ fvs = tracker_views.MakeBounceFieldValueViews(parsed_fvs, {}, config)
+ self.assertTrue(fvs[0].applicable)
+
+
+class ConvertLabelsToFieldValuesTest(unittest.TestCase):
+
+ def testConvertLabelsToFieldValues_NoLabels(self):
+ result = tracker_views._ConvertLabelsToFieldValues(
+ [], 'opsys', {})
+ self.assertEqual([], result)
+
+ def testConvertLabelsToFieldValues_NoMatch(self):
+ result = tracker_views._ConvertLabelsToFieldValues(
+ [], 'opsys', {})
+ self.assertEqual([], result)
+
+ def testConvertLabelsToFieldValues_HasMatch(self):
+ result = tracker_views._ConvertLabelsToFieldValues(
+ ['OSX'], 'opsys', {})
+ self.assertEqual(1, len(result))
+ self.assertEqual('OSX', result[0].val)
+ self.assertEqual('', result[0].docstring)
+
+ result = tracker_views._ConvertLabelsToFieldValues(
+ ['OSX', 'All'], 'opsys', {'opsys-all': 'Happens everywhere'})
+ self.assertEqual(2, len(result))
+ self.assertEqual('OSX', result[0].val)
+ self.assertEqual('', result[0].docstring)
+ self.assertEqual('All', result[1].val)
+ self.assertEqual('Happens everywhere', result[1].docstring)
+
+
+class FieldDefViewTest(unittest.TestCase):
+
+ def setUp(self):
+ self.approval_fd = tracker_bizobj.MakeFieldDef(
+ 1, 789, 'LaunchApproval', tracker_pb2.FieldTypes.APPROVAL_TYPE, None,
+ None, True, True, False, 3, 99, None, False, None, None,
+ None, 'no_action', 'descriptive docstring', False, None, False)
+
+ self.approval_def = tracker_pb2.ApprovalDef(
+ approval_id=1, approver_ids=[111], survey='question?')
+
+ self.field_def = tracker_bizobj.MakeFieldDef(
+ 2, 789, 'AffectedUsers', tracker_pb2.FieldTypes.INT_TYPE, None,
+ None, True, True, False, 3, 99, None, False, None, None,
+ None, 'no_action', 'descriptive docstring', False, 1, False)
+
+ self.field_def.admin_ids = [222]
+ self.field_def.editor_ids = [111, 333]
+
+ def testFieldDefView_Normal(self):
+ config = _MakeConfig()
+ config.field_defs.append(self.approval_fd)
+ config.approval_defs.append(self.approval_def)
+
+ user_view_1 = framework_views.StuffUserView(111, 'uv1@example.com', False)
+ user_view_2 = framework_views.StuffUserView(222, 'uv2@example.com', False)
+ user_view_3 = framework_views.StuffUserView(333, 'uv3@example.com', False)
+ user_views = {111: user_view_1, 222: user_view_2, 333: user_view_3}
+ view = tracker_views.FieldDefView(
+ self.field_def, config, user_views=user_views)
+
+ self.assertEqual('AffectedUsers', view.field_name)
+ self.assertEqual(self.field_def, view.field_def)
+ self.assertEqual('descriptive docstring', view.docstring_short)
+ self.assertEqual('INT_TYPE', view.type_name)
+ self.assertEqual([], view.choices)
+ self.assertEqual('required', view.importance)
+ self.assertEqual(3, view.min_value)
+ self.assertEqual(99, view.max_value)
+ self.assertEqual('no_action', view.date_action_str)
+ self.assertEqual(view.approval_id, 1)
+ self.assertEqual(view.is_approval_subfield, ezt.boolean(True))
+ self.assertEqual(view.approvers, [])
+ self.assertEqual(view.survey, '')
+ self.assertEqual(view.survey_questions, [])
+ self.assertEqual(len(view.admins), 1)
+ self.assertEqual(len(view.editors), 2)
+ self.assertIsNone(view.is_phase_field)
+ self.assertIsNone(view.is_restricted_field)
+
+ def testFieldDefView_Approval(self):
+ config = _MakeConfig()
+ approver_view = framework_views.StuffUserView(
+ 111, 'shouldnotmatter@ch.org', False)
+ user_views = {111: approver_view}
+
+ view = tracker_views.FieldDefView(
+ self.approval_fd, config,
+ user_views= user_views, approval_def=self.approval_def)
+ self.assertEqual(view.approvers, [approver_view])
+ self.assertEqual(view.survey, self.approval_def.survey)
+ self.assertEqual(view.survey_questions, [view.survey])
+
+ self.approval_def.survey = None
+ view = tracker_views.FieldDefView(
+ self.approval_fd, config,
+ user_views= user_views, approval_def=self.approval_def)
+ self.assertEqual(view.survey, '')
+ self.assertEqual(view.survey_questions, [])
+
+ self.approval_def.survey = 'Q1\nQ2\nQ3'
+ view = tracker_views.FieldDefView(
+ self.approval_fd, config,
+ user_views= user_views, approval_def=self.approval_def)
+ self.assertEqual(view.survey, self.approval_def.survey)
+ self.assertEqual(view.survey_questions, ['Q1', 'Q2', 'Q3'])
+
+
+class IssueTemplateViewTest(unittest.TestCase):
+ pass # TODO(jrobbins): write tests
+
+
+class MakeFieldUserViewsTest(unittest.TestCase):
+ pass # TODO(jrobbins): write tests
+
+
+class ConfigViewTest(unittest.TestCase):
+ pass # TODO(jrobbins): write tests
+
+
+class ConfigFunctionsTest(unittest.TestCase):
+
+ def setUp(self):
+ self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(768)
+
+ def testStatusDefsAsText(self):
+ open_text, closed_text = tracker_views.StatusDefsAsText(self.config)
+
+ for wks in tracker_constants.DEFAULT_WELL_KNOWN_STATUSES:
+ status, doc, means_open, _deprecated = wks
+ if means_open:
+ self.assertIn(status, open_text)
+ self.assertIn(doc, open_text)
+ else:
+ self.assertIn(status, closed_text)
+ self.assertIn(doc, closed_text)
+
+ self.assertEqual(
+ len(tracker_constants.DEFAULT_WELL_KNOWN_STATUSES),
+ len(open_text.split('\n')) + len(closed_text.split('\n')))
+
+ def testLabelDefsAsText(self):
+ # Note: Day-Monday will not be part of the result because it is masked.
+ self.config.field_defs.append(tracker_pb2.FieldDef(
+ field_id=1, field_name='Day',
+ field_type=tracker_pb2.FieldTypes.ENUM_TYPE))
+ self.config.well_known_labels.append(tracker_pb2.LabelDef(
+ label='Day-Monday'))
+ labels_text = tracker_views.LabelDefsAsText(self.config)
+
+ for wkl in tracker_constants.DEFAULT_WELL_KNOWN_LABELS:
+ label, doc, _deprecated = wkl
+ self.assertIn(label, labels_text)
+ self.assertIn(doc, labels_text)
+ self.assertEqual(
+ len(tracker_constants.DEFAULT_WELL_KNOWN_LABELS),
+ len(labels_text.split('\n')))
diff --git a/tracker/test/webcomponentspage_test.py b/tracker/test/webcomponentspage_test.py
new file mode 100644
index 0000000..65cfc66
--- /dev/null
+++ b/tracker/test/webcomponentspage_test.py
@@ -0,0 +1,120 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+"""Tests for the Monorail SPA pages, as served by EZT."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mock
+import unittest
+
+import ezt
+
+import settings
+from framework import permissions
+from proto import project_pb2
+from proto import site_pb2
+from services import service_manager
+from tracker import webcomponentspage
+from testing import fake
+from testing import testing_helpers
+
+
+class WebComponentsPageTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ project=fake.ProjectService(),
+ features=fake.FeaturesService())
+
+ self.user = self.services.user.TestAddUser('user@example.com', 111)
+ self.project = self.services.project.TestAddProject('proj', project_id=789)
+ self.hotlist = self.services.features.TestAddHotlist(
+ 'HotlistName', summary='summary', owner_ids=[111], hotlist_id=1236)
+
+ self.servlet = webcomponentspage.WebComponentsPage(
+ 'req', 'res', services=self.services)
+
+ def testHotlistPage_OldUiUrl(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 111},
+ path='/hotlists/1236',
+ services=self.services)
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual('/u/111/hotlists/HotlistName', page_data['old_ui_url'])
+
+ def testHotlistPage_OldUiUrl_People(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 111},
+ path='/hotlists/1236/people',
+ services=self.services)
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(
+ '/u/111/hotlists/HotlistName/people', page_data['old_ui_url'])
+
+ def testHotlistPage_OldUiUrl_Settings(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 111},
+ path='/hotlists/1236/settings',
+ services=self.services)
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(
+ '/u/111/hotlists/HotlistName/details', page_data['old_ui_url'])
+
+
+class ProjectListPageTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(project=fake.ProjectService())
+
+ self.project_a = self.services.project.TestAddProject('a', project_id=1)
+ self.project_b = self.services.project.TestAddProject('b', project_id=2)
+
+ self.servlet = webcomponentspage.ProjectListPage(
+ 'req', 'res', services=self.services)
+
+ @mock.patch('settings.domain_to_default_project', {})
+ def testMaybeRedirectToDomainDefaultProject_NoMatch(self):
+ """No redirect if the user is not accessing via a configured domain."""
+ mr = testing_helpers.MakeMonorailRequest()
+ mr.request.host = 'example.com'
+ msg = self.servlet._MaybeRedirectToDomainDefaultProject(mr)
+ print('msg: ' + msg)
+ self.assertTrue(msg.startswith('No configured'))
+
+ @mock.patch('settings.domain_to_default_project', {'example.com': 'huh'})
+ def testMaybeRedirectToDomainDefaultProject_NoSuchProject(self):
+ """No redirect if the configured project does not exist."""
+ mr = testing_helpers.MakeMonorailRequest()
+ mr.request.host = 'example.com'
+ print('host is %r' % mr.request.host)
+ msg = self.servlet._MaybeRedirectToDomainDefaultProject(mr)
+ print('msg: ' + msg)
+ self.assertTrue(msg.endswith('not found'))
+
+ @mock.patch('settings.domain_to_default_project', {'example.com': 'a'})
+ def testMaybeRedirectToDomainDefaultProject_CantView(self):
+ """No redirect if the user can't view the configured project."""
+ self.project_a.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ mr = testing_helpers.MakeMonorailRequest()
+ mr.request.host = 'example.com'
+ msg = self.servlet._MaybeRedirectToDomainDefaultProject(mr)
+ print('msg: ' + msg)
+ self.assertTrue(msg.startswith('User cannot'))
+
+ @mock.patch('settings.domain_to_default_project', {'example.com': 'a'})
+ def testMaybeRedirectToDomainDefaultProject_Redirect(self):
+ """We redirect if there's a configured project that the user can view."""
+ mr = testing_helpers.MakeMonorailRequest()
+ mr.request.host = 'example.com'
+ self.servlet.redirect = mock.Mock()
+ msg = self.servlet._MaybeRedirectToDomainDefaultProject(mr)
+ print('msg: ' + msg)
+ self.assertTrue(msg.startswith('Redirected'))
+ self.servlet.redirect.assert_called_once()