Merge branch 'main' into avm99963-monorail

Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266

GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/tracker/test/tracker_helpers_test.py b/tracker/test/tracker_helpers_test.py
index 4f89cc9..b7e930b 100644
--- a/tracker/test/tracker_helpers_test.py
+++ b/tracker/test/tracker_helpers_test.py
@@ -1,7 +1,6 @@
-# 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
+# Copyright 2022 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unittest for the tracker helpers module."""
 from __future__ import print_function
@@ -11,6 +10,8 @@
 import copy
 import mock
 import unittest
+import io
+import six
 
 import settings
 
@@ -21,15 +22,16 @@
 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 mrproto import project_pb2
+from mrproto import tracker_pb2
+from mrproto 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
+from werkzeug.datastructures import FileStorage
 
 TEST_ID_MAP = {
     'a@example.com': 1,
@@ -166,39 +168,39 @@
 
     add, remove = tracker_helpers._ClassifyPlusMinusItems(
         ['', ' ', '  \t', '-'])
-    self.assertItemsEqual([], add)
-    self.assertItemsEqual([], remove)
+    six.assertCountEqual(self, [], add)
+    six.assertCountEqual(self, [], remove)
 
     add, remove = tracker_helpers._ClassifyPlusMinusItems(
         ['a', 'b', 'c'])
-    self.assertItemsEqual(['a', 'b', 'c'], add)
-    self.assertItemsEqual([], remove)
+    six.assertCountEqual(self, ['a', 'b', 'c'], add)
+    six.assertCountEqual(self, [], remove)
 
     add, remove = tracker_helpers._ClassifyPlusMinusItems(
         ['a-a-a', 'b-b', 'c-'])
-    self.assertItemsEqual(['a-a-a', 'b-b', 'c-'], add)
-    self.assertItemsEqual([], remove)
+    six.assertCountEqual(self, ['a-a-a', 'b-b', 'c-'], add)
+    six.assertCountEqual(self, [], remove)
 
     add, remove = tracker_helpers._ClassifyPlusMinusItems(
         ['-a'])
-    self.assertItemsEqual([], add)
-    self.assertItemsEqual(['a'], remove)
+    six.assertCountEqual(self, [], add)
+    six.assertCountEqual(self, ['a'], remove)
 
     add, remove = tracker_helpers._ClassifyPlusMinusItems(
         ['-a', 'b', 'c-c'])
-    self.assertItemsEqual(['b', 'c-c'], add)
-    self.assertItemsEqual(['a'], remove)
+    six.assertCountEqual(self, ['b', 'c-c'], add)
+    six.assertCountEqual(self, ['a'], remove)
 
     add, remove = tracker_helpers._ClassifyPlusMinusItems(
         ['-a', '-b-b', '-c-'])
-    self.assertItemsEqual([], add)
-    self.assertItemsEqual(['a', 'b-b', 'c-'], remove)
+    six.assertCountEqual(self, [], add)
+    six.assertCountEqual(self, ['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)
+    six.assertCountEqual(self, ['a'], add)
+    six.assertCountEqual(self, ['a'], remove)
 
   def testParseIssueRequestFields(self):
     parsed_fields = tracker_helpers._ParseIssueRequestFields(fake.PostData({
@@ -227,22 +229,25 @@
             }}))
 
   def testParseIssueRequestAttachments(self):
-    file1 = testing_helpers.Blank(
+    file1 = FileStorage(
+        stream=io.BytesIO(b'hello world'),
         filename='hello.c',
-        value='hello world')
-
-    file2 = testing_helpers.Blank(
+    )
+    file2 = FileStorage(
+        stream=io.BytesIO(b'Welcome to our project'),
         filename='README',
-        value='Welcome to our project')
+    )
 
-    file3 = testing_helpers.Blank(
+    file3 = FileStorage(
+        stream=io.BytesIO(b'Abort, Retry, or Fail?'),
         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(
+    file4 = FileStorage(
+        stream=io.BytesIO(b''),
         filename='',
-        value='')
+    )
 
     attachments = tracker_helpers._ParseIssueRequestAttachments({})
     self.assertEqual([], attachments)
@@ -250,26 +255,31 @@
     attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
         'file1': [file1],
         }))
-    self.assertEqual(
-        [('hello.c', 'hello world', 'text/plain')],
-        attachments)
+    self.assertEqual([('hello.c', b'hello world', 'text/plain')], attachments)
+    file1.seek(0)
 
     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)
+        [
+            ('hello.c', b'hello world', 'text/plain'),
+            ('README', b'Welcome to our project', 'text/plain')
+        ], attachments)
+    file1.seek(0)
+    file2.seek(0)
 
     attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
         'file3': [file3],
         }))
     self.assertEqual(
-        [('FILENAME.EXT', 'Abort, Retry, or Fail?',
-          'application/octet-stream')],
-        attachments)
+        [
+            (
+                'FILENAME.EXT', b'Abort, Retry, or Fail?',
+                'application/octet-stream')
+        ], attachments)
+    file3.seek(0)
 
     attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
         'file1': [file4],  # Does not appear in result
@@ -277,9 +287,12 @@
         'file4': [file4],  # Does not appear in result
         }))
     self.assertEqual(
-        [('FILENAME.EXT', 'Abort, Retry, or Fail?',
-          'application/octet-stream')],
-        attachments)
+        [
+            (
+                'FILENAME.EXT', b'Abort, Retry, or Fail?',
+                'application/octet-stream')
+        ], attachments)
+    file3.seek(0)
 
   def testParseIssueRequestKeptAttachments(self):
     pass  # TODO(jrobbins): Write this test.
@@ -368,12 +381,12 @@
     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)
+    six.assertCountEqual(
+        self, ['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)
+    six.assertCountEqual(
+        self, [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)
 
@@ -386,11 +399,12 @@
     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)
+    six.assertCountEqual(
+        self, ['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)
+    six.assertCountEqual(
+        self, [TEST_ID_MAP['c@example.com'], gen_uid], parsed_users.cc_ids)
     self.assertEqual([], parsed_users.cc_ids_remove)
 
     post_data = fake.PostData({
@@ -398,12 +412,12 @@
         })
     parsed_users = tracker_helpers._ParseIssueRequestUsers(
         'fake connection', post_data, self.services)
-    self.assertItemsEqual(
-        ['c@example.com', 'b@example.com'], parsed_users.cc_usernames)
+    six.assertCountEqual(
+        self, ['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)
+    six.assertCountEqual(
+        self, [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):
@@ -698,9 +712,9 @@
   @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')
+        'matter not', b'three men make a tiger', 'matter not')
     upload_2 = framework_helpers.AttachmentUpload(
-        'matter not', 'chicken', 'matter not')
+        'matter not', b'chicken', 'matter not')
     attachments = [upload_1, upload_2]
 
     project = fake.Project()
@@ -713,7 +727,7 @@
     self.assertEqual(actual_new, expected_new)
 
     upload_3 = framework_helpers.AttachmentUpload(
-        'matter not', 'donut', 'matter not')
+        'matter not', b'donut', 'matter not')
     attachments.append(upload_3)
     with self.assertRaises(exceptions.OverAttachmentQuota):
       tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
@@ -722,7 +736,7 @@
       'tracker.tracker_constants.ISSUE_ATTACHMENTS_QUOTA_HARD', len('tiger'))
   def testComputeNewQuotaBytesUsed_GeneralQuota(self):
     upload_1 = framework_helpers.AttachmentUpload(
-        'matter not', 'tiger', 'matter not')
+        'matter not', b'tiger', 'matter not')
     attachments = [upload_1]
 
     project = fake.Project()
@@ -732,13 +746,13 @@
     self.assertEqual(actual_new, expected_new)
 
     upload_2 = framework_helpers.AttachmentUpload(
-        'matter not', 'donut', 'matter not')
+        'matter not', b'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')
+        'matter not', b'donut', 'matter not')
     attachments.append(upload_3)
     with self.assertRaises(exceptions.OverAttachmentQuota):
       tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
@@ -901,7 +915,7 @@
 
   # ParseMergeFields is tested in IssueMergeTest.
   # AddIssueStarrers is tested in IssueMergeTest.testMergeIssueStars().
-  # IsMergeAllowed is tested in IssueMergeTest.
+  # CanEditProjectIssue is tested in IssueMergeTest.
 
   def testPairDerivedValuesWithRuleExplanations_Nothing(self):
     """Test we return nothing for an issue with no derived values."""
@@ -990,8 +1004,9 @@
     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()))
+    six.assertCountEqual(
+        self, [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)
 
@@ -999,8 +1014,8 @@
     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()))
+    six.assertCountEqual(
+        self, [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)
 
@@ -1008,7 +1023,7 @@
     issue_list = []
     users_by_id = tracker_helpers.MakeViewsForUsersInIssues(
         'fake cnxn', issue_list, self.user)
-    self.assertItemsEqual([], list(users_by_id.keys()))
+    six.assertCountEqual(self, [], list(users_by_id.keys()))
 
 
 class GetAllIssueProjectsTest(unittest.TestCase):
@@ -1236,19 +1251,34 @@
     self.assertEqual(str(mergee_issue.local_id), text)
     self.assertEqual(mergee_issue, merge_into_issue)
 
-  def testIsMergeAllowed(self):
+  def testCanEditProjectIssue(self):
     mr = testing_helpers.MakeMonorailRequest()
-    issue = fake.MakeTestIssue(987, 1, 'summary', 'New', 111)
+    issue = fake.MakeTestIssue(
+        self.project.project_id, 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)
+    non_member_not_allowed = tracker_helpers.CanEditProjectIssue(
+        mr, self.project, issue, None)
+    self.assertEqual(False, non_member_not_allowed)
+
+    committer_id = 3
+    self.project.committer_ids.extend([committer_id])
+    mr.auth.effective_ids.add(committer_id)
+    committer_allowed = tracker_helpers.CanEditProjectIssue(
+        mr, self.project, issue, None)
+    self.assertEqual(True, committer_allowed)
+
+    self.project.state = project_pb2.ProjectState.ARCHIVED
+    committer_read_only_not_allowed = tracker_helpers.CanEditProjectIssue(
+        mr, self.project, issue, None)
+    self.assertEqual(False, committer_read_only_not_allowed)
+
+    owner_id = 1
+    self.project.owner_ids.extend([owner_id])
+    mr.auth.effective_ids.add(owner_id)
+    owner_read_only_not_allowed = tracker_helpers.CanEditProjectIssue(
+        mr, self.project, issue, None)
+    self.assertEqual(False, owner_read_only_not_allowed)
 
   def testMergeIssueStars(self):
     mr = testing_helpers.MakeMonorailRequest()
@@ -1276,13 +1306,13 @@
 
     new_starrers = tracker_helpers.GetNewIssueStarrers(
         self.cnxn, self.services, [1, 3], 2)
-    self.assertItemsEqual(new_starrers, [1, 2, 6])
+    six.assertCountEqual(self, 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)
+    six.assertCountEqual(self, [1, 2, 3, 4, 5, 6], issue_2_starrers)
 
 
 class MergeLinkedMembersTest(unittest.TestCase):
@@ -1345,58 +1375,61 @@
   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)
+    six.assertCountEqual(
+        self, [
+            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(
+    six.assertCountEqual(
+        self,
         [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)
+    six.assertCountEqual(self, self.all_emails, visible_members)
 
     visible_members = self.DoFiltering(
         permissions.ADMIN_PERMISSIONSET)
-    self.assertItemsEqual(self.all_emails, visible_members)
+    six.assertCountEqual(self, 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)
+    six.assertCountEqual(self, self.all_emails, visible_members)
 
     visible_members = self.DoFiltering(
         permissions.ADMIN_PERMISSIONSET)
-    self.assertItemsEqual(self.all_emails, visible_members)
+    six.assertCountEqual(self, self.all_emails, visible_members)
 
     visible_members = self.DoFiltering(
         permissions.COMMITTER_ACTIVE_PERMISSIONSET)
-    self.assertItemsEqual(self.all_emails, visible_members)
+    six.assertCountEqual(self, self.all_emails, visible_members)
 
   def testNonOwnersCanSeeAll_NormalProject(self):
     visible_members = self.DoFiltering(
         permissions.COMMITTER_ACTIVE_PERMISSIONSET)
-    self.assertItemsEqual(self.all_emails, visible_members)
+    six.assertCountEqual(self, self.all_emails, visible_members)
 
     visible_members = self.DoFiltering(
         permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET)
-    self.assertItemsEqual(self.all_emails, visible_members)
+    six.assertCountEqual(self, 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(
+    six.assertCountEqual(
+        self,
         [self.owner_email, self.committer_email, self.indirect_member_email],
         visible_members)
 
@@ -1618,29 +1651,44 @@
     tracker_helpers.AssertValidIssueForCreate(
         self.cnxn, self.services, input_issue, 'nonempty description')
 
+  def testAssertValidIssueForCreate_ValidatesLabels(self):
+    input_issue = tracker_pb2.Issue(
+        summary='sum',
+        labels=['freeze_new_label'],
+        status='New',
+        owner_id=111,
+        project_id=789)
+    with self.assertRaisesRegex(
+        exceptions.InputException,
+        ("The creation of new labels is blocked for the Chromium project"
+         " in Monorail. To continue with editing your issue, please"
+         " remove: freeze_new_label label\\(s\\)")):
+      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'):
+    with self.assertRaisesRegex(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'):
+    with self.assertRaisesRegex(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'):
+    with self.assertRaisesRegex(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'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                'Summary is required'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, 'nonempty description')
       input_issue.summary = '   '
@@ -1650,8 +1698,8 @@
   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'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                'Description is required'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, '')
       tracker_helpers.AssertValidIssueForCreate(
@@ -1678,8 +1726,8 @@
       return None
 
     self.services.config.LookupStatusID = mock_status_lookup
-    with self.assertRaisesRegexp(exceptions.InputException,
-                                 'Undefined status: DNE_status'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                'Undefined status: DNE_status'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, 'nonempty description')
 
@@ -1691,9 +1739,8 @@
         owner_id=111,
         project_id=789,
         component_ids=[3])
-    with self.assertRaisesRegexp(
-        exceptions.InputException,
-        'Undefined or deprecated component with id: 3'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                'Undefined or deprecated component with id: 3'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, 'nonempty description')
 
@@ -1704,9 +1751,8 @@
         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'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                'Undefined or deprecated component with id: 2'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, 'nonempty description')
 
@@ -1728,8 +1774,8 @@
                 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: .+'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                r'users/123: .+\nusers/124: .+'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, 'nonempty description')
     self.assertEqual(input_issue, copied_issue)
@@ -1894,7 +1940,7 @@
     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_merge_add.cc_ids = sorted([expected_main.owner_id, 111, 333])
     expected_merged_from_add[expected_merge_add.issue_id] = [
         issue_main.issue_id
     ]
@@ -2086,9 +2132,9 @@
     ]
 
     upload_1 = framework_helpers.AttachmentUpload(
-        'dragon', 'OOOOOO\n', 'text/plain')
+        'dragon', b'OOOOOO\n', 'text/plain')
     upload_2 = framework_helpers.AttachmentUpload(
-        'snake', 'ooooo\n', 'text/plain')
+        'snake', b'ooooo\n', 'text/plain')
     attachment_uploads = [upload_1, upload_2]
 
     actual = tracker_helpers._EnforceAttachmentQuotaLimits(
@@ -2118,13 +2164,13 @@
     ]
 
     upload_1 = framework_helpers.AttachmentUpload(
-        'dragon', 'OOOOOO\n', 'text/plain')
+        'dragon', b'OOOOOO\n', 'text/plain')
     upload_2 = framework_helpers.AttachmentUpload(
-        'snake', 'ooooo\n', 'text/plain')
+        'snake', b'ooooo\n', 'text/plain')
     attachment_uploads = [upload_1, upload_2]
 
-    with self.assertRaisesRegexp(exceptions.OverAttachmentQuota,
-                                 r'.+ project Patroclus\n.+ project Circe'):
+    with self.assertRaisesRegex(exceptions.OverAttachmentQuota,
+                                r'.+ project Patroclus\n.+ project Circe'):
       tracker_helpers._EnforceAttachmentQuotaLimits(
           self.cnxn, issue_delta_pairs, self.services, attachment_uploads)
 
@@ -2225,6 +2271,21 @@
             delta_8, delta_9, delta_10, delta_11
         ])
 
+  def testAssertIssueChangesValid_ValidatesLabels(self):
+    """Asserts labels."""
+    issue_1 = _Issue('chicken', 1)
+    self.services.issue.TestAddIssue(issue_1)
+    delta_1 = tracker_pb2.IssueDelta(labels_add=['freeze_new_label'])
+    issue_delta_pairs = [(issue_1, delta_1)]
+    comment = 'just a plain comment'
+    with self.assertRaisesRegex(
+        exceptions.InputException,
+        ("The creation of new labels is blocked for the Chromium project"
+         " in Monorail. To continue with editing your issue, please"
+         " remove: freeze_new_label label\\(s\\).")):
+      tracker_helpers._AssertIssueChangesValid(
+          self.cnxn, issue_delta_pairs, self.services, comment_content=comment)
+
   def testAssertIssueChangesValid_RequiredField(self):
     """Asserts fields and requried fields.."""
     issue_1 = _Issue('chicken', 1)
@@ -2323,8 +2384,8 @@
         '%s: MERGED type statuses must accompany mergedInto values.' %
         issue_3_ref)
 
-    with self.assertRaisesRegexp(exceptions.InputException,
-                                 '\n'.join(expected_err_msgs)):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                '\n'.join(expected_err_msgs)):
       tracker_helpers._AssertIssueChangesValid(
           self.cnxn, issue_delta_pairs, self.services, comment_content=comment)
 
@@ -2390,8 +2451,8 @@
         (issue_7, delta_7),
     ]
 
-    with self.assertRaisesRegexp(exceptions.InputException,
-                                 '\n'.join(expected_err_msgs)):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                '\n'.join(expected_err_msgs)):
       tracker_helpers._AssertIssueChangesValid(
           self.cnxn, issue_delta_pairs, self.services)
 
@@ -2426,13 +2487,13 @@
 
     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])
+    six.assertCountEqual(self, 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, [])
+    six.assertCountEqual(self, new_cc_ids, [])
 
   def testEnforceNonMergeStatusDeltas(self):
     # No updates: user is setting to a non-MERGED status with no
@@ -2692,7 +2753,7 @@
             [('proj', m_remove.local_id)], default_project_name='proj')
         ]
     self.assertEqual(actual_amendments, expected_amendments)
-    self.assertItemsEqual(actual_new_starrers, [333, 444])
+    six.assertCountEqual(self, actual_new_starrers, [333, 444])
 
     expected_issue.cc_ids.append(777)
     expected_issue.blocked_on_iids = [78404, bo_add.issue_id]
@@ -2767,7 +2828,7 @@
     dne_users = [2, 3]
     existing = [1, 1001, 1002, 1003, 2001, 2002, 3002]
     all_users = existing + dne_users
-    with self.assertRaisesRegexp(
+    with self.assertRaisesRegex(
         exceptions.InputException,
         'users/2: User does not exist.\nusers/3: User does not exist.'):
       with exceptions.ErrorAggregator(exceptions.InputException) as err_agg: