Merge branch 'main' into avm99963-monorail

Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266

GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/services/test/api_pb2_v1_helpers_test.py b/services/test/api_pb2_v1_helpers_test.py
index 460f5c3..ac94d57 100644
--- a/services/test/api_pb2_v1_helpers_test.py
+++ b/services/test/api_pb2_v1_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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for the API v1 helpers."""
 from __future__ import print_function
@@ -17,10 +16,10 @@
 from framework import profiler
 from services import api_pb2_v1_helpers
 from services import service_manager
-from proto import api_pb2_v1
-from proto import project_pb2
-from proto import tracker_pb2
-from proto import usergroup_pb2
+from mrproto import api_pb2_v1
+from mrproto import project_pb2
+from mrproto import tracker_pb2
+from mrproto import usergroup_pb2
 from testing import fake
 from tracker import tracker_bizobj
 
@@ -279,7 +278,8 @@
     # TODO(jrobbins): set up a lot more fields.
 
     for cls in [api_pb2_v1.IssueWrapper, api_pb2_v1.IssuesGetInsertResponse]:
-      result = api_pb2_v1_helpers.convert_issue(cls, issue, mar, self.services)
+      result = api_pb2_v1_helpers.convert_issue(
+          cls, issue, mar, self.services, migrated_id='12345')
       self.assertEqual(1, result.id)
       self.assertEqual('one', result.title)
       self.assertEqual('one', result.summary)
@@ -323,6 +323,7 @@
           [api_pb2_v1.Phase(phaseName="JustAPhase", rank=4),
            api_pb2_v1.Phase(phaseName="NotAPhase", rank=9)
           ])
+      self.assertEqual('12345', result.migrated_id)
 
       # TODO(jrobbins): check a lot more fields.
 
diff --git a/services/test/api_svc_v1_test.py b/services/test/api_svc_v1_test.py
index b7cd9b1..72f7aee 100644
--- a/services/test/api_svc_v1_test.py
+++ b/services/test/api_svc_v1_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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for the API v1."""
 from __future__ import print_function
@@ -9,9 +8,11 @@
 from __future__ import absolute_import
 
 import datetime
+from unittest import mock
 import endpoints
 import logging
 from mock import Mock, patch, ANY
+import six
 import time
 import unittest
 import webtest
@@ -27,9 +28,9 @@
 from framework import permissions
 from framework import profiler
 from framework import template_helpers
-from proto import api_pb2_v1
-from proto import project_pb2
-from proto import tracker_pb2
+from mrproto import api_pb2_v1
+from mrproto import project_pb2
+from mrproto import tracker_pb2
 from search import frontendsearchpipeline
 from services import api_svc_v1
 from services import service_manager
@@ -40,6 +41,7 @@
 from testing_utils import testing
 from tracker import tracker_bizobj
 from tracker import tracker_constants
+from redirect import redirect_utils
 
 
 def MakeFakeServiceManager():
@@ -163,7 +165,7 @@
     oauth.get_current_user.side_effect = oauth.Error()
     with self.assertRaises(webtest.AppError) as cm:
       self.call_api('users_get', self.request)
-    self.assertTrue(cm.exception.message.startswith('Bad response: 401'))
+    self.assertTrue(str(cm.exception).startswith('Bad response: 401'))
 
 
 class MonorailApiTest(testing.EndpointsTestCase):
@@ -198,6 +200,7 @@
               lambda x, y, z, u, v, w: ('id', 'email'))
 
     self.mock(tracker_fulltext, 'IndexIssues', lambda x, y, z, u, v: None)
+    self.mock(tracker_fulltext, 'UnindexIssues', lambda _: None)
 
   def SetUpComponents(
       self, project_id, component_id, component_name, component_doc='doc',
@@ -303,6 +306,85 @@
     self.assertEqual('Field1', resp['fieldValues'][0]['fieldName'])
     self.assertEqual('11', resp['fieldValues'][0]['fieldValue'])
 
+  @mock.patch('businesslogic.work_env.WorkEnv.GetIssueMigratedID')
+  def testIssuesGet_GetIssue_MigratedId(self, mockGetIssueMigratedId):
+    """Get the requested issue."""
+    mockGetIssueMigratedId.return_value = '23456'
+
+    self.services.project.TestAddProject(
+        'test-project', owner_ids=[222], project_id=12345)
+    self.SetUpComponents(12345, 1, 'API')
+    self.SetUpFieldDefs(1, 12345, 'Field1', tracker_pb2.FieldTypes.INT_TYPE)
+
+    fv = tracker_pb2.FieldValue(field_id=1, int_value=11)
+    issue1 = fake.MakeTestIssue(
+        project_id=12345,
+        local_id=1,
+        owner_id=222,
+        reporter_id=111,
+        status='New',
+        summary='sum',
+        component_ids=[1],
+        field_values=[fv])
+    self.services.issue.TestAddIssue(issue1)
+
+    resp = self.call_api('issues_get', self.request).json_body
+    self.assertEqual(1, resp['id'])
+    self.assertEqual('New', resp['status'])
+    self.assertEqual('open', resp['state'])
+    self.assertFalse(resp['canEdit'])
+    self.assertTrue(resp['canComment'])
+    self.assertEqual('requester@example.com', resp['author']['name'])
+    self.assertEqual('user@example.com', resp['owner']['name'])
+    self.assertEqual('API', resp['components'][0])
+    self.assertEqual('Field1', resp['fieldValues'][0]['fieldName'])
+    self.assertEqual('11', resp['fieldValues'][0]['fieldValue'])
+    self.assertEqual('23456', resp['migrated_id'])
+
+  @patch('framework.cloud_tasks_helpers.create_task')
+  def testIssuesInsert_FreezeLabels(self, _create_task_mock):
+    """Attempts to add new labels are blocked"""
+    self.services.project.TestAddProject(
+        'test-project', owner_ids=[222], committer_ids=[111], project_id=999)
+    self.SetUpFieldDefs(1, 999, 'Field1', tracker_pb2.FieldTypes.INT_TYPE)
+
+    issue1 = fake.MakeTestIssue(
+        project_id=999,
+        local_id=1,
+        owner_id=222,
+        reporter_id=111,
+        status='New',
+        summary='Test issue')
+    self.services.issue.TestAddIssue(issue1)
+
+    issue_dict = {
+        'blockedOn': [{
+            'issueId': 1
+        }],
+        'cc': [{
+            'name': 'user@example.com'
+        }, {
+            'name': ''
+        }, {
+            'name': ' '
+        }],
+        'description': 'description',
+        'labels': ['freeze_new_label', 'label1'],
+        'owner': {
+            'name': 'requester@example.com'
+        },
+        'status': 'New',
+        'summary': 'Test issue',
+        'fieldValues': [{
+            'fieldName': 'Field1',
+            'fieldValue': '11'
+        }]
+    }
+    self.request.update(issue_dict)
+
+    with self.call_should_fail(400):
+      self.call_api('issues_insert', self.request)
+
   def testIssuesInsert_BadRequest(self):
     """The request does not specify summary or status."""
 
@@ -573,6 +655,36 @@
     with self.call_should_fail(403):
       self.call_api('issues_comments_insert', self.request)
 
+  def testIssuesCommentsInsert_ArchivedProject(self):
+    """No permission to comment in an archived project."""
+    self.services.project.TestAddProject(
+        'test-project',
+        owner_ids=[111],
+        state=project_pb2.ProjectState.ARCHIVED,
+        project_id=12345)
+    issue1 = fake.MakeTestIssue(12345, 1, 'Issue 1', 'New', 2)
+    self.services.issue.TestAddIssue(issue1)
+
+    self.services.project.TestAddProject(
+        'archived-project', owner_ids=[222], project_id=6789)
+    issue2 = fake.MakeTestIssue(
+        6789, 2, 'Issue 2', 'New', 222, project_name='archived-project')
+    self.services.issue.TestAddIssue(issue2)
+
+    self.request['updates'] = {
+        'blockedOn': ['archived-project:2'],
+        'mergedInto': '',
+    }
+    with self.call_should_fail(403):
+      self.call_api('issues_comments_insert', self.request)
+
+    self.request['updates'] = {
+        'blockedOn': [],
+        'mergedInto': 'archived-project:2',
+    }
+    with self.call_should_fail(403):
+      self.call_api('issues_comments_insert', self.request)
+
   def testIssuesCommentsInsert_CommentPermissionOnly(self):
     """User has permission to comment, even though they cannot edit."""
     self.services.project.TestAddProject(
@@ -600,6 +712,28 @@
     with self.call_should_fail(400):
       self.call_api('issues_comments_insert', self.request)
 
+  def testIssuesCommentsInsert_FreezeLabels(self):
+    """Attempts to add new labels are blocked"""
+    self.services.project.TestAddProject(
+        'test-project', owner_ids=[111], project_id=999)
+
+    issue1 = fake.MakeTestIssue(
+        999, 1, 'Issue 1', 'New', 222, project_name='test-project')
+    self.services.issue.TestAddIssue(issue1)
+
+    self.request['updates'] = {
+        'summary': 'new summary',
+        'status': 'Started',
+        'owner': 'requester@example.com',
+        'cc': ['user@example.com'],
+        'labels': ['freeze_new_label', '-remove_label'],
+        'blockedOn': ['2'],
+        'blocking': ['3'],
+    }
+
+    with self.call_should_fail(400):
+      self.call_api('issues_comments_insert', self.request)
+
   def testIssuesCommentsInsert_Amendments_Normal(self):
     """Insert comments with amendments."""
 
@@ -703,10 +837,10 @@
     self.assertEqual(2, len(issue2_comments))  # description and merge
     source_starrers = self.services.issue_star.LookupItemStarrers(
         'cnxn', issue1.issue_id)
-    self.assertItemsEqual([111, 222, 333], source_starrers)
+    six.assertCountEqual(self, [111, 222, 333], source_starrers)
     target_starrers = self.services.issue_star.LookupItemStarrers(
         'cnxn', issue2.issue_id)
-    self.assertItemsEqual([111, 222, 333, 555], target_starrers)
+    six.assertCountEqual(self, [111, 222, 333, 555], target_starrers)
 
   def testIssuesCommentsInsert_CustomFields(self):
     """Update custom field values."""
@@ -1470,9 +1604,13 @@
     with self.call_should_fail(403):
       self.call_api('groups_create', self.request)
 
-  def SetUpGroupRequest(self, group_name, who_can_view_members='MEMBERS',
-                        ext_group_type=None, perms=None,
-                        requester='requester@example.com'):
+  def SetUpGroupRequest(
+      self,
+      group_name,
+      who_can_view_members='MEMBERS',
+      ext_group_type='CHROME_INFRA_AUTH',
+      perms=None,
+      requester='requester@example.com'):
     request = {
         'groupName': group_name,
         'requester': requester,
@@ -1648,7 +1786,8 @@
     cd_dict = {
       'componentPath': 'API'}
     self.request.update(cd_dict)
-    _ = self.call_api('components_delete', self.request).json_body
+    with self.assertWarns(webtest.lint.WSGIWarning):
+      _ = self.call_api('components_delete', self.request)
     self.assertEqual(0, len(self.config.component_defs))
 
   def testComponentsUpdate_Invalid(self):
@@ -1704,12 +1843,13 @@
               'requester@example.com', 'user@example.com', '', ' ']},
           {'field': 'DEPRECATED', 'deprecated': True}]}
     self.request.update(cd_dict)
-    _ = self.call_api('components_update', self.request).json_body
+    with self.assertWarns(webtest.lint.WSGIWarning):
+      _ = self.call_api('components_update', self.request)
     component_def = tracker_bizobj.FindComponentDef(
         'API', self.config)
     self.assertIsNotNone(component_def)
     self.assertEqual('', component_def.docstring)
-    self.assertItemsEqual([111, 222], component_def.cc_ids)
+    six.assertCountEqual(self, [111, 222], component_def.cc_ids)
     self.assertTrue(component_def.deprecated)
 
     cd_dict = {
@@ -1717,7 +1857,8 @@
       'updates': [
           {'field': 'LEAF_NAME', 'leafName': 'NewParent'}]}
     self.request.update(cd_dict)
-    _ = self.call_api('components_update', self.request).json_body
+    with self.assertWarns(webtest.lint.WSGIWarning):
+      _ = self.call_api('components_update', self.request)
     cd_parent = tracker_bizobj.FindComponentDef(
         'NewParent', self.config)
     cd_child = tracker_bizobj.FindComponentDef(
@@ -1838,6 +1979,21 @@
       api_svc_v1.api_base_checks(
           request, requester, self.services, None, self.auth_client_ids, [])
 
+  def testNonLiveMigratedProject(self):
+    archived_project = 'archived-migrated-project'
+    redirect_utils.PROJECT_REDIRECT_MAP = {
+        'archived-migrated-project': 'https://example.dev'
+    }
+    self.services.project.TestAddProject(
+        archived_project,
+        owner_ids=[111],
+        state=project_pb2.ProjectState.ARCHIVED)
+    request = RequestMock()
+    request.projectId = archived_project
+    requester = RequesterMock(email='test@example.com')
+    api_svc_v1.api_base_checks(
+        request, requester, self.services, None, self.auth_client_ids, [])
+
   def testNoViewProjectPermission(self):
     nonmember_email = 'nonmember@example.com'
     self.services.user.TestAddUser(nonmember_email, 222)
diff --git a/services/test/cachemanager_svc_test.py b/services/test/cachemanager_svc_test.py
index b84d33e..bd66be4 100644
--- a/services/test/cachemanager_svc_test.py
+++ b/services/test/cachemanager_svc_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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for the cachemanager service."""
 from __future__ import print_function
diff --git a/services/test/caches_test.py b/services/test/caches_test.py
index cd401be..23f793c 100644
--- a/services/test/caches_test.py
+++ b/services/test/caches_test.py
@@ -1,13 +1,13 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for the cache classes."""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
+import six
 import unittest
 
 from google.appengine.api import memcache
@@ -141,10 +141,10 @@
     self.assertEqual(3, len(self.sharded_ram_cache.cache))
 
 
-class TestableTwoLevelCache(caches.AbstractTwoLevelCache):
+class _TestableTwoLevelCache(caches.AbstractTwoLevelCache):
 
   def __init__(self, cache_manager, kind, max_size=None):
-    super(TestableTwoLevelCache, self).__init__(
+    super(_TestableTwoLevelCache, self).__init__(
         cache_manager, kind, 'testable:', None, max_size=max_size)
 
   # pylint: disable=unused-argument
@@ -162,7 +162,7 @@
 
     self.cnxn = 'fake connection'
     self.cache_manager = fake.CacheManager()
-    self.testable_2lc = TestableTwoLevelCache(self.cache_manager, 'issue')
+    self.testable_2lc = _TestableTwoLevelCache(self.cache_manager, 'issue')
 
   def tearDown(self):
     self.testbed.deactivate()
@@ -239,8 +239,9 @@
     self.assertEqual({123: 12300, 124: 12400, 333: 333, 444: 444}, hits)
     self.assertEqual([], misses)
     # The RAM cache now has items found in memcache and DB.
-    self.assertItemsEqual(
-        [123, 124, 125, 333, 444], list(self.testable_2lc.cache.cache.keys()))
+    six.assertCountEqual(
+        self, [123, 124, 125, 333, 444],
+        list(self.testable_2lc.cache.cache.keys()))
 
   def testGetAll_FetchGetsItFromDB(self):
     self.testable_2lc.CacheItem(123, 12300)
diff --git a/services/test/chart_svc_test.py b/services/test/chart_svc_test.py
index 470bc80..8392481 100644
--- a/services/test/chart_svc_test.py
+++ b/services/test/chart_svc_test.py
@@ -1,8 +1,7 @@
 # -*- coding: utf-8 -*-
-# 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
+# Copyright 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for chart_svc module."""
 from __future__ import print_function
@@ -25,8 +24,8 @@
 from services import service_manager
 from framework import permissions
 from framework import sql
-from proto import ast_pb2
-from proto import tracker_pb2
+from mrproto import ast_pb2
+from mrproto import tracker_pb2
 from search import ast2select
 from search import search_helpers
 from testing import fake
diff --git a/services/test/client_config_svc_test.py b/services/test/client_config_svc_test.py
index d8a305e..fbcd2f9 100644
--- a/services/test/client_config_svc_test.py
+++ b/services/test/client_config_svc_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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for the client config service."""
 from __future__ import print_function
@@ -9,6 +8,8 @@
 from __future__ import absolute_import
 
 import base64
+import binascii
+import six
 import unittest
 
 from services import client_config_svc
@@ -20,33 +21,39 @@
     def __init__(self, content):
       self.content = content
 
+  def testProcessResponse_InvalidContent(self):
+    r = self.FakeResponse('')
+    with self.assertRaises(AttributeError):
+      client_config_svc._process_response(r)
+
   def testProcessResponse_InvalidJSON(self):
-    r = self.FakeResponse('}{')
+    r = self.FakeResponse(b')]}\'}{')
     with self.assertRaises(ValueError):
       client_config_svc._process_response(r)
 
   def testProcessResponse_NoContent(self):
-    r = self.FakeResponse('{"wrong-key": "some-value"}')
+    r = self.FakeResponse(b')]}\'{"wrong-key": "some-value"}')
     with self.assertRaises(KeyError):
       client_config_svc._process_response(r)
 
   def testProcessResponse_NotB64(self):
     # 'asd' is not a valid base64-encoded string.
-    r = self.FakeResponse('{"content": "asd"}')
-    with self.assertRaises(TypeError):
+    r = self.FakeResponse(b')]}\'{"rawContent": "asd"}')
+    with self.assertRaises(binascii.Error):
       client_config_svc._process_response(r)
 
   def testProcessResponse_NotProto(self):
     # 'asdf' is a valid base64-encoded string.
-    r = self.FakeResponse('{"content": "asdf"}')
-    with self.assertRaises(Exception):
+    r = self.FakeResponse(b')]}\'{"rawContent": "asdf"}')
+    with self.assertRaises(UnicodeDecodeError):
       client_config_svc._process_response(r)
 
   def testProcessResponse_Success(self):
-    with open(client_config_svc.CONFIG_FILE_PATH) as f:
-      r = self.FakeResponse('{"content": "%s"}' % base64.b64encode(f.read()))
+    with open(client_config_svc.CONFIG_FILE_PATH, 'rb') as f:
+      r = self.FakeResponse(
+          b')]}\'{"rawContent": "%s"}' % base64.b64encode(f.read()))
     c = client_config_svc._process_response(r)
-    assert '123456789.apps.googleusercontent.com' in c
+    assert b'123456789.apps.googleusercontent.com' in c
 
 
 class ClientConfigServiceTest(unittest.TestCase):
diff --git a/services/test/config_svc_test.py b/services/test/config_svc_test.py
index dd2796c..4100d3e 100644
--- a/services/test/config_svc_test.py
+++ b/services/test/config_svc_test.py
@@ -1,17 +1,17 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for config_svc module."""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
-import re
-import unittest
 import logging
 import mock
+import re
+import six
+import unittest
 
 try:
   from mox3 import mox
@@ -24,7 +24,7 @@
 from framework import exceptions
 from framework import framework_constants
 from framework import sql
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from services import config_svc
 from services import template_svc
 from testing import fake
@@ -220,7 +220,7 @@
         self.componentdef_rows, self.component2admin_rows,
         self.component2cc_rows, self.component2label_rows,
         self.approvaldef2approver_rows, self.approvaldef2survey_rows)
-    self.assertItemsEqual([789], list(config_dict.keys()))
+    six.assertCountEqual(self, [789], list(config_dict.keys()))
     config = config_dict[789]
     self.assertEqual(789, config.project_id)
     self.assertEqual(['Duplicate'], config.statuses_offer_merge)
@@ -280,7 +280,7 @@
     self.mox.ReplayAll()
     config_dict = self.config_2lc._FetchConfigs(self.cnxn, keys)
     self.mox.VerifyAll()
-    self.assertItemsEqual(keys, list(config_dict.keys()))
+    six.assertCountEqual(self, keys, list(config_dict.keys()))
 
   def testFetchItems(self):
     keys = [678, 789]
@@ -288,7 +288,7 @@
     self.mox.ReplayAll()
     config_dict = self.config_2lc.FetchItems(self.cnxn, keys)
     self.mox.VerifyAll()
-    self.assertItemsEqual(keys, list(config_dict.keys()))
+    six.assertCountEqual(self, keys, list(config_dict.keys()))
 
 
 class ConfigServiceTest(unittest.TestCase):
@@ -441,6 +441,22 @@
         self.cnxn, 789, 'NewLabel', autocreate=False))
     self.mox.VerifyAll()
 
+  def testLookupLabelID_CaseSensitive(self):
+    label_dicts = {101: 'security', 201: 'ux'}, {'security': 101, 'ux': 201}
+    self.config_service.label_cache.CacheItem(789, label_dicts)
+
+    self.config_service.labeldef_tbl.Select(
+        self.cnxn,
+        cols=['id'],
+        project_id=789,
+        where=[('label = %s', ['Security'])],
+        limit=1).AndReturn([])
+    self.mox.ReplayAll()
+    self.assertIsNone(
+        self.config_service.LookupLabelID(
+            self.cnxn, 789, 'Security', autocreate=False, case_sensitive=True))
+    self.mox.VerifyAll()
+
   def testLookupLabelIDs_Hit(self):
     label_dicts = {1: 'Security', 2: 'UX'}, {'security': 1, 'ux': 2}
     self.config_service.label_cache.CacheItem(789, label_dicts)
@@ -456,16 +472,16 @@
     self.config_service.label_cache.CacheItem(789, label_dicts)
     # No mock calls set up because none are needed.
     self.mox.ReplayAll()
-    self.assertItemsEqual(
-        [1],
+    six.assertCountEqual(
+        self, [1],
         self.config_service.LookupIDsOfLabelsMatching(
             self.cnxn, 789, re.compile('Sec.*')))
-    self.assertItemsEqual(
-        [1, 2],
+    six.assertCountEqual(
+        self, [1, 2],
         self.config_service.LookupIDsOfLabelsMatching(
             self.cnxn, 789, re.compile('.*')))
-    self.assertItemsEqual(
-        [],
+    six.assertCountEqual(
+        self, [],
         self.config_service.LookupIDsOfLabelsMatching(
             self.cnxn, 789, re.compile('Zzzzz.*')))
     self.mox.VerifyAll()
@@ -789,9 +805,7 @@
     with self.assertRaises(exceptions.InputException) as cm:
       self.config_service._UpdateWellKnownLabels(self.cnxn, config)
     self.mox.VerifyAll()
-    self.assertEqual(
-      'Defined label "Type-Defect" twice',
-      cm.exception.message)
+    self.assertEqual('Defined label "Type-Defect" twice', str(cm.exception))
 
   def testUpdateWellKnownStatuses(self):
     config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
@@ -1001,7 +1015,7 @@
     comp_ids = self.config_service.FindMatchingComponentIDsAnyProject(
         self.cnxn, ['WindowManager', 'NetworkLayer'])
     self.mox.VerifyAll()
-    self.assertItemsEqual([1, 2, 3], comp_ids)
+    six.assertCountEqual(self, [1, 2, 3], comp_ids)
 
   def testFindMatchingComponentIDsAnyProject_NonRooted(self):
     self.SetUpFindMatchingComponentIDsAnyProject(False, [(1,), (2,), (3,)])
@@ -1010,7 +1024,7 @@
     comp_ids = self.config_service.FindMatchingComponentIDsAnyProject(
         self.cnxn, ['WindowManager', 'NetworkLayer'], exact=False)
     self.mox.VerifyAll()
-    self.assertItemsEqual([1, 2, 3], comp_ids)
+    six.assertCountEqual(self, [1, 2, 3], comp_ids)
 
   def SetUpCreateComponentDef(self, comp_id):
     self.config_service.componentdef_tbl.InsertRow(
diff --git a/services/test/features_svc_test.py b/services/test/features_svc_test.py
index d285152..fcd0546 100644
--- a/services/test/features_svc_test.py
+++ b/services/test/features_svc_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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for features_svc module."""
 from __future__ import print_function
@@ -13,6 +12,7 @@
   from mox3 import mox
 except ImportError:
   import mox
+import six
 import time
 import unittest
 import mock
@@ -27,8 +27,8 @@
 from framework import exceptions
 from framework import framework_constants
 from framework import sql
-from proto import tracker_pb2
-from proto import features_pb2
+from mrproto import tracker_pb2
+from mrproto import features_pb2
 from services import chart_svc
 from services import features_svc
 from services import star_svc
@@ -82,14 +82,14 @@
     hotlist_dict = self.features_service.hotlist_2lc._DeserializeHotlists(
         hotlist_rows, issue_rows, role_rows)
 
-    self.assertItemsEqual([123, 234], list(hotlist_dict.keys()))
+    six.assertCountEqual(self, [123, 234], list(hotlist_dict.keys()))
     self.assertEqual(123, hotlist_dict[123].hotlist_id)
     self.assertEqual('hot1', hotlist_dict[123].name)
-    self.assertItemsEqual([111, 444], hotlist_dict[123].owner_ids)
-    self.assertItemsEqual([222], hotlist_dict[123].editor_ids)
-    self.assertItemsEqual([333], hotlist_dict[123].follower_ids)
+    six.assertCountEqual(self, [111, 444], hotlist_dict[123].owner_ids)
+    six.assertCountEqual(self, [222], hotlist_dict[123].editor_ids)
+    six.assertCountEqual(self, [333], hotlist_dict[123].follower_ids)
     self.assertEqual(234, hotlist_dict[234].hotlist_id)
-    self.assertItemsEqual([111], hotlist_dict[234].owner_ids)
+    six.assertCountEqual(self, [111], hotlist_dict[234].owner_ids)
 
 
 class HotlistIDTwoLevelCache(unittest.TestCase):
@@ -138,12 +138,12 @@
 
     # Assertions
     self.features_service.hotlist2user_tbl.Select.assert_called_once_with(
-        self.cnxn, cols=['hotlist_id', 'user_id'], user_id=[555, 333, 222],
+        self.cnxn, cols=['hotlist_id', 'user_id'], user_id=[222, 333, 555],
         role_name='owner')
     hotlist_ids = [123, 124, 125, 126, 127]
     self.features_service.hotlist_tbl.Select.assert_called_once_with(
         self.cnxn, cols=['id', 'name'], id=hotlist_ids, is_deleted=False,
-        where=[('LOWER(name) IN (%s,%s)', ['name3', 'name1'])])
+        where=[('LOWER(name) IN (%s,%s)', ['name1', 'name3'])])
 
     self.assertEqual(hit,{
         ('name1', 111): 121,
@@ -635,7 +635,7 @@
         17: [tracker_pb2.FilterRule(
             predicate=rows[3][2], add_cc_ids=[111, 222])],
     }
-    self.assertItemsEqual(rules_dict, expected_dict)
+    six.assertCountEqual(self, rules_dict, expected_dict)
 
     self.features_service.filterrule_tbl.Select.assert_called_once_with(
         self.cnxn, features_svc.FILTERRULE_COLS)
@@ -667,7 +667,7 @@
     emails = {'cow@fart.test': 222}
     rules_dict = self.features_service.ExpungeFilterRulesByUser(
         self.cnxn, emails)
-    self.assertItemsEqual(rules_dict, {})
+    six.assertCountEqual(self, rules_dict, {})
 
     self.features_service.filterrule_tbl.Select.assert_called_once_with(
         self.cnxn, features_svc.FILTERRULE_COLS)
@@ -773,7 +773,7 @@
         self.cnxn, ['q3-todo', 'Q4-TODO'], [222, 333, 444])
     self.assertEqual(ret, {('q3-todo', 222) : 123, ('q4-todo', 333): 124})
     self.features_service.hotlist2user_tbl.Select.assert_called_once_with(
-        self.cnxn, cols=['hotlist_id', 'user_id'], user_id=[444, 333, 222],
+        self.cnxn, cols=['hotlist_id', 'user_id'], user_id=[222, 333, 444],
         role_name='owner')
     self.features_service.hotlist_tbl.Select.assert_called_once_with(
         self.cnxn, cols=['id', 'name'], id=[123, 125], is_deleted=False,
@@ -965,7 +965,7 @@
     hotlist_dict = self.features_service.GetHotlists(
         self.cnxn, [123, 456])
     self.mox.VerifyAll()
-    self.assertItemsEqual([123, 456], list(hotlist_dict.keys()))
+    six.assertCountEqual(self, [123, 456], list(hotlist_dict.keys()))
     self.assertEqual('hotlist1', hotlist_dict[123].name)
     self.assertEqual('hotlist2', hotlist_dict[456].name)
 
@@ -1306,7 +1306,7 @@
     self.features_service.GetProjectIDsFromHotlist = mock.Mock(
         return_value=[hotlists_project_id])
 
-    hotlist_ids = hotlists_by_id.keys()
+    hotlist_ids = list(hotlists_by_id.keys())
     commit = True  # commit in ExpungeHotlists should be True by default.
     self.features_service.ExpungeHotlists(
         self.cnxn, hotlist_ids, star_service, user_service, chart_service)
diff --git a/services/test/fulltext_helpers_test.py b/services/test/fulltext_helpers_test.py
index fbff1b8..42febf4 100644
--- a/services/test/fulltext_helpers_test.py
+++ b/services/test/fulltext_helpers_test.py
@@ -1,13 +1,13 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for the fulltext_helpers module."""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
+import six
 import unittest
 
 try:
@@ -17,8 +17,8 @@
 
 from google.appengine.api import search
 
-from proto import ast_pb2
-from proto import tracker_pb2
+from mrproto import ast_pb2
+from mrproto import tracker_pb2
 from search import query2ast
 from services import fulltext_helpers
 
@@ -247,4 +247,4 @@
     project_ids = fulltext_helpers.ComprehensiveSearch(
         'browser', 'search index name')
     self.mox.VerifyAll()
-    self.assertItemsEqual([123, 234, 345], project_ids)
+    six.assertCountEqual(self, [123, 234, 345], project_ids)
diff --git a/services/test/issue_svc_test.py b/services/test/issue_svc_test.py
index fe41aa4..f6b6c29 100644
--- a/services/test/issue_svc_test.py
+++ b/services/test/issue_svc_test.py
@@ -1,8 +1,7 @@
 # -*- coding: utf-8 -*-
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for issue_svc module."""
 
@@ -11,6 +10,7 @@
 from __future__ import absolute_import
 
 import logging
+import six
 import time
 import unittest
 from mock import patch, Mock, ANY
@@ -27,7 +27,7 @@
 from framework import exceptions
 from framework import framework_constants
 from framework import sql
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from services import caches
 from services import chart_svc
 from services import issue_svc
@@ -63,12 +63,12 @@
   return issue_service
 
 
-class TestableIssueTwoLevelCache(issue_svc.IssueTwoLevelCache):
+class _TestableIssueTwoLevelCache(issue_svc.IssueTwoLevelCache):
 
   def __init__(self, issue_list):
     cache_manager = fake.CacheManager()
-    super(TestableIssueTwoLevelCache, self).__init__(
-        cache_manager, None, None, None)
+    super(_TestableIssueTwoLevelCache,
+          self).__init__(cache_manager, None, None, None)
     self.cache = caches.RamCache(cache_manager, 'issue')
     self.memcache_prefix = 'issue:'
     self.pb_class = tracker_pb2.Issue
@@ -134,8 +134,8 @@
     issue_dict = self.issue_id_2lc.FetchItems(
         self.cnxn, project_local_ids_list)
     self.mox.VerifyAll()
-    self.assertItemsEqual(project_local_ids_list, list(issue_dict.keys()))
-    self.assertItemsEqual(issue_ids, list(issue_dict.values()))
+    six.assertCountEqual(self, project_local_ids_list, list(issue_dict.keys()))
+    six.assertCountEqual(self, issue_ids, list(issue_dict.values()))
 
   def testKeyToStr(self):
     self.assertEqual('789,1', self.issue_id_2lc._KeyToStr((789, 1)))
@@ -161,9 +161,10 @@
     now = int(time.time())
     self.project_service.TestAddProject('proj', project_id=789)
     self.issue_rows = [
-        (78901, 789, 1, 1, 111, 222,
-         now, now, now, now, now, now,
-         0, 0, 0, 1, 0, False)]
+        (
+            78901, 789, 1, 1, 111, 222, now, now, now, now, now, now, now, 0, 0,
+            0, 1, 0, False)
+    ]
     self.summary_rows = [(78901, 'sum')]
     self.label_rows = [(78901, 1, 0)]
     self.component_rows = []
@@ -224,14 +225,14 @@
         self.component_rows, self.cc_rows, self.notify_rows,
         self.fieldvalue_rows, self.relation_rows, self.dangling_relation_rows,
         self.phase_rows, self.approvalvalue_rows, self.av_approver_rows)
-    self.assertItemsEqual([78901], list(issue_dict.keys()))
+    six.assertCountEqual(self, [78901], list(issue_dict.keys()))
     issue = issue_dict[78901]
     self.assertEqual(len(issue.phases), 2)
     self.assertIsNotNone(tracker_bizobj.FindPhaseByID(1, issue.phases))
     av_21 = tracker_bizobj.FindApprovalValueByID(
         21, issue.approval_values)
     self.assertEqual(av_21.phase_id, 1)
-    self.assertItemsEqual(av_21.approver_ids, [111, 222, 333])
+    six.assertCountEqual(self, av_21.approver_ids, [111, 222, 333])
     self.assertIsNotNone(tracker_bizobj.FindPhaseByID(2, issue.phases))
     self.assertEqual(issue.phases,
                      [tracker_pb2.Phase(rank=1, phase_id=1, name='Canary'),
@@ -356,7 +357,7 @@
     self.mox.ReplayAll()
     issue_dict = self.issue_2lc.FetchItems(self.cnxn, issue_ids)
     self.mox.VerifyAll()
-    self.assertItemsEqual(issue_ids, list(issue_dict.keys()))
+    six.assertCountEqual(self, issue_ids, list(issue_dict.keys()))
     self.assertEqual(2, len(issue_dict[78901].phases))
 
   def testFetchItemsNoApprovalValues(self):
@@ -365,7 +366,7 @@
     self.mox.ReplayAll()
     issue_dict = self.issue_2lc.FetchItems(self.cnxn, issue_ids)
     self.mox.VerifyAll()
-    self.assertItemsEqual(issue_ids, list(issue_dict.keys()))
+    six.assertCountEqual(self, issue_ids, list(issue_dict.keys()))
     self.assertEqual([], issue_dict[78901].phases)
 
 
@@ -750,7 +751,7 @@
   def testGetIssuesDict(self):
     issue_ids = [78901, 78902, 78903]
     issue_1, issue_2 = self.SetUpGetIssues()
-    self.services.issue.issue_2lc = TestableIssueTwoLevelCache(
+    self.services.issue.issue_2lc = _TestableIssueTwoLevelCache(
         [issue_1, issue_2])
     issues_dict, missed_iids = self.services.issue.GetIssuesDict(
         self.cnxn, issue_ids)
@@ -827,10 +828,9 @@
   def SetUpInsertIssue(
       self, label_rows=None, av_rows=None, approver_rows=None,
       dangling_relation_rows=None):
-    row = (789, 1, 1, 111, 111,
-           self.now, 0, self.now, self.now, self.now, self.now,
-           None, 0,
-           False, 0, 0, False)
+    row = (
+        789, 1, 1, 111, 111, self.now, 0, self.now, self.now, self.now,
+        self.now, self.now, None, 0, False, 0, 0, False)
     self.services.issue.issue_tbl.InsertRows(
         self.cnxn, issue_svc.ISSUE_COLS[1:], [row],
         commit=False, return_generated_ids=True).AndReturn([78901])
@@ -852,9 +852,9 @@
         commit=False)
 
   def SetUpInsertSpamIssue(self):
-    row = (789, 1, 1, 111, 111,
-           self.now, 0, self.now, self.now, self.now, self.now,
-           None, 0, False, 0, 0, True)
+    row = (
+        789, 1, 1, 111, 111, self.now, 0, self.now, self.now, self.now,
+        self.now, self.now, None, 0, False, 0, 0, True)
     self.services.issue.issue_tbl.InsertRows(
         self.cnxn, issue_svc.ISSUE_COLS[1:], [row],
         commit=False, return_generated_ids=True).AndReturn([78901])
@@ -972,13 +972,14 @@
         'owner_modified': 123456789,
         'status_modified': 123456789,
         'component_modified': 123456789,
+        'migration_modified': 123456789,
         'derived_owner_id': None,
         'derived_status_id': None,
         'deleted': False,
         'star_count': 12,
         'attachment_count': 0,
         'is_spam': False,
-        }
+    }
     self.services.issue.issue_tbl.Update(
         self.cnxn, delta, id=78901, commit=False)
     if not given_delta:
@@ -1006,9 +1007,15 @@
 
   def testUpdateIssues_Normal(self):
     issue = fake.MakeTestIssue(
-        project_id=789, local_id=1, owner_id=111, summary='sum',
-        status='Live', labels=['Type-Defect'], issue_id=78901,
-        opened_timestamp=123456789, modified_timestamp=123456789,
+        project_id=789,
+        local_id=1,
+        owner_id=111,
+        summary='sum',
+        status='Live',
+        labels=['Type-Defect'],
+        issue_id=78901,
+        opened_timestamp=123456789,
+        modified_timestamp=123456789,
         star_count=12)
     issue.assume_stale = False
     self.SetUpUpdateIssues()
@@ -1018,9 +1025,15 @@
 
   def testUpdateIssue_Normal(self):
     issue = fake.MakeTestIssue(
-        project_id=789, local_id=1, owner_id=111, summary='sum',
-        status='Live', labels=['Type-Defect'], issue_id=78901,
-        opened_timestamp=123456789, modified_timestamp=123456789,
+        project_id=789,
+        local_id=1,
+        owner_id=111,
+        summary='sum',
+        status='Live',
+        labels=['Type-Defect'],
+        issue_id=78901,
+        opened_timestamp=123456789,
+        modified_timestamp=123456789,
         star_count=12)
     issue.assume_stale = False
     self.SetUpUpdateIssues()
@@ -1030,9 +1043,15 @@
 
   def testUpdateIssue_Stale(self):
     issue = fake.MakeTestIssue(
-        project_id=789, local_id=1, owner_id=111, summary='sum',
-        status='Live', labels=['Type-Defect'], issue_id=78901,
-        opened_timestamp=123456789, modified_timestamp=123456789,
+        project_id=789,
+        local_id=1,
+        owner_id=111,
+        summary='sum',
+        status='Live',
+        labels=['Type-Defect'],
+        issue_id=78901,
+        opened_timestamp=123456789,
+        modified_timestamp=123456789,
         star_count=12)
     # Do not set issue.assume_stale = False
     # Do not call self.SetUpUpdateIssues() because nothing should be updated.
@@ -1270,7 +1289,8 @@
         7890101, is_description=True, approval_id=7,
         content=config.approval_defs[2].survey, commit=False)
     amendment_row = (
-        78901, 7890101, 'custom', None, '-Llama Roo', None, None, 'Approvals')
+        78901, 7890101, 'custom', None, '-Llama Roo', None, None, 'Approvals',
+        None, None)
     self.SetUpInsertComment(
         7890101, content=comment_content, amendment_rows=[amendment_row],
         commit=False)
@@ -1473,8 +1493,10 @@
 
     # Calls in ApplyIssueDelta
     # Call to find added blocking issues.
-    issue_refs = {blocking_issue: (
-        blocking_issue.project_name, blocking_issue.local_id)}
+    issue_refs = {
+        blocking_issue.issue_id:
+            (blocking_issue.project_name, blocking_issue.local_id)
+    }
     self.services.issue.LookupIssueRefs(
         self.cnxn, [blocking_issue.issue_id]).AndReturn(issue_refs)
     # Call to find removed blocking issues.
@@ -1636,10 +1658,10 @@
   def testSoftDeleteIssue(self):
     project = fake.Project(project_id=789)
     issue_1, issue_2 = self.SetUpGetIssues()
-    self.services.issue.issue_2lc = TestableIssueTwoLevelCache(
+    self.services.issue.issue_2lc = _TestableIssueTwoLevelCache(
         [issue_1, issue_2])
     self.services.issue.issue_id_2lc.CacheItem((789, 1), 78901)
-    delta = {'deleted': True}
+    delta = {'deleted': True, 'migration_modified': self.now}
     self.services.issue.issue_tbl.Update(
         self.cnxn, delta, id=78901, commit=False)
 
@@ -1842,7 +1864,10 @@
     commentcontent_rows = [(7890101, 'content', 'msg'),
                            (7890102, 'content2', 'msg')]
     amendment_rows = [
-        (1, 78901, 7890101, 'cc', 'old', 'new val', 222, None, None)]
+        (
+            1, 78901, 7890101, 'cc', 'old', 'new val', 222, None, None, None,
+            None)
+    ]
     attachment_rows = []
     approval_rows = [(23, 7890102)]
     importer_rows = []
@@ -1869,6 +1894,24 @@
     self.assertEqual(2, len(comments))
     self.assertEqual(222, comments[0].importer_id)
 
+  def testUpackAmendment(self):
+    amendment_row = (
+        1, 78901, 7890101, 'cc', 'old', 'new val', 222, None, None, None, None)
+    amendment, comment_id = self.services.issue._UnpackAmendment(amendment_row)
+    self.assertEqual(comment_id, 7890101)
+    self.assertEqual(amendment.field, tracker_pb2.FieldID('CC'))
+    self.assertEqual(amendment.newvalue, 'new val')
+    self.assertEqual(amendment.oldvalue, 'old')
+    self.assertEqual(amendment.added_user_ids, [222])
+
+  def testUpackAmendment_With_Unicode(self):
+    amendment_row = (
+        1, 78901, 7890102, 'custom', None, None, None, None, None, u'123', None)
+    amendment, comment_id = self.services.issue._UnpackAmendment(amendment_row)
+    self.assertEqual(comment_id, 7890102)
+    self.assertEqual(amendment.field, tracker_pb2.FieldID('CUSTOM'))
+    self.assertEqual(amendment.added_component_ids, [123])
+
   def MockTheRestOfGetCommentsByID(self, comment_ids):
     self.services.issue.commentcontent_tbl.Select = Mock(
         return_value=[
@@ -2117,6 +2160,32 @@
     self.mox.VerifyAll()
     self.assertEqual(7890101, comment.id)
 
+  def testInsertComment_WithIssueUpdate(self):
+    amendment = tracker_bizobj.MakeAmendment(
+        tracker_pb2.FieldID.COMPONENTS, 'aaa', [], [], added_component_ids=[1])
+    amendment_rows = [
+        (
+            78901, 7890101, 'components', None, 'aaa', None, None, None, None,
+            None),
+        (78901, 7890101, 'components', None, None, None, None, None, 1, None)
+    ]
+    comment = tracker_pb2.IssueComment(
+        issue_id=78901,
+        timestamp=self.now,
+        project_id=789,
+        user_id=111,
+        content='content',
+        amendments=[amendment])
+    self.services.issue.commentcontent_tbl.InsertRow = Mock(
+        return_value=78901010)
+    self.services.issue.comment_tbl.InsertRow = Mock(return_value=7890101)
+    self.services.issue.issueupdate_tbl.InsertRows = Mock()
+
+    self.services.issue.InsertComment(self.cnxn, comment, commit=True)
+
+    self.services.issue.issueupdate_tbl.InsertRows.assert_called_once_with(
+        self.cnxn, issue_svc.ISSUEUPDATE_COLS[1:], amendment_rows, commit=False)
+
   def SetUpUpdateComment(self, comment_id, delta=None):
     delta = delta or {
         'commenter_id': 111,
@@ -2189,7 +2258,7 @@
   def testSoftDeleteComment(self):
     """Deleting a comment with an attachment marks it and updates count."""
     issue_1, issue_2 = self.SetUpGetIssues()
-    self.services.issue.issue_2lc = TestableIssueTwoLevelCache(
+    self.services.issue.issue_2lc = _TestableIssueTwoLevelCache(
         [issue_1, issue_2])
     issue_1.attachment_count = 1
     issue_1.assume_stale = False
@@ -2198,7 +2267,11 @@
     self.services.issue.issue_id_2lc.CacheItem((789, 1), 78901)
     self.SetUpUpdateComment(
         comment.id, delta={'deleted_by': 222, 'is_spam': False})
-    self.SetUpUpdateIssues(given_delta={'attachment_count': 0})
+    self.SetUpUpdateIssues(
+        given_delta={
+            'attachment_count': 0,
+            'migration_modified': self.now
+        })
     self.SetUpEnqueueIssuesForIndexing([78901])
     self.mox.ReplayAll()
     self.services.issue.SoftDeleteComment(
@@ -2418,7 +2491,11 @@
     comment.attachments.append(attachment)
 
     self.SetUpUpdateAttachment(179901, 1234, {'deleted': True})
-    self.SetUpUpdateIssues(given_delta={'attachment_count': 0})
+    self.SetUpUpdateIssues(
+        given_delta={
+            'attachment_count': 0,
+            'migration_modified': self.now
+        })
     self.SetUpEnqueueIssuesForIndexing([78901])
 
     self.mox.ReplayAll()
@@ -2626,6 +2703,9 @@
     self.services.issue.issueapproval2approver_tbl.Delete = Mock()
     self.services.issue.issue2approvalvalue_tbl.Update = Mock()
 
+    issue_update_id_rows = [(78914,), (78915,)]
+    self.services.issue.issueupdate_tbl.Select = Mock(
+        return_value=issue_update_id_rows)
     self.services.issue.issueupdate_tbl.Update = Mock()
 
     self.services.issue.issue2notify_tbl.Delete = Mock()
@@ -2652,18 +2732,19 @@
     commit = False
     limit = 50
 
-    affected_user_ids = self.services.issue.ExpungeUsersInIssues(
+    affected_issue_ids = self.services.issue.ExpungeUsersInIssues(
         self.cnxn, user_ids_by_email, limit=limit)
-    self.assertItemsEqual(
-        affected_user_ids,
-        [78901, 78902, 78903, 78904, 78905, 78906, 78907, 78908, 78909,
-         78910, 78911, 78912, 78913])
+    six.assertCountEqual(
+        self, affected_issue_ids, [
+            78901, 78902, 78903, 78904, 78905, 78906, 78907, 78908, 78909,
+            78910, 78911, 78912, 78913, 78914, 78915
+        ])
 
     self.services.issue.comment_tbl.Select.assert_called_once()
     _cnxn, kwargs = self.services.issue.comment_tbl.Select.call_args
     self.assertEqual(
         kwargs['cols'], ['Comment.id', 'Comment.issue_id', 'commentcontent_id'])
-    self.assertItemsEqual(kwargs['commenter_id'], user_ids)
+    six.assertCountEqual(self, kwargs['commenter_id'], user_ids)
     self.assertEqual(kwargs['limit'], limit)
 
     # since user_ids are passed to ExpungeUsersInIssues via a dictionary,
@@ -2723,9 +2804,6 @@
         self.cnxn, {'reporter_id': framework_constants.DELETED_USER_ID},
         id=[row[0] for row in reporter_issue_id_rows], commit=commit)
 
-    self.assertEqual(
-        3, len(self.services.issue.issue_tbl.Update.call_args_list))
-
     # issue updates
     self.services.issue.issueupdate_tbl.Update.assert_any_call(
         self.cnxn, {'added_user_id': framework_constants.DELETED_USER_ID},
@@ -2736,11 +2814,19 @@
     self.assertEqual(
         2, len(self.services.issue.issueupdate_tbl.Update.call_args_list))
 
+    # check updates across all issues
+    self.services.issue.issue_tbl.Update.assert_any_call(
+        self.cnxn, {'migration_modified': self.now},
+        id=affected_issue_ids,
+        commit=commit)
+    self.assertEqual(
+        4, len(self.services.issue.issue_tbl.Update.call_args_list))
+
     # issue notify
     call_args_list = self.services.issue.issue2notify_tbl.Delete.call_args_list
     self.assertEqual(1, len(call_args_list))
     _cnxn, kwargs = call_args_list[0]
-    self.assertItemsEqual(kwargs['email'], emails)
+    six.assertCountEqual(self, kwargs['email'], emails)
     self.assertEqual(kwargs['commit'], commit)
 
     # issue snapshots
diff --git a/services/test/project_svc_test.py b/services/test/project_svc_test.py
index 48de180..3ada8ea 100644
--- a/services/test/project_svc_test.py
+++ b/services/test/project_svc_test.py
@@ -1,13 +1,13 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for the project_svc module."""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
+import six
 import time
 import unittest
 
@@ -21,8 +21,8 @@
 
 from framework import framework_constants
 from framework import sql
-from proto import project_pb2
-from proto import user_pb2
+from mrproto import project_pb2
+from mrproto import user_pb2
 from services import config_svc
 from services import project_svc
 from testing import fake
@@ -79,15 +79,15 @@
     project_dict = self.project_service.project_2lc._DeserializeProjects(
         project_rows, role_rows, extraperm_rows)
 
-    self.assertItemsEqual([123, 234], list(project_dict.keys()))
+    six.assertCountEqual(self, [123, 234], list(project_dict.keys()))
     self.assertEqual(123, project_dict[123].project_id)
     self.assertEqual('proj1', project_dict[123].project_name)
     self.assertEqual(NOW, project_dict[123].recent_activity)
-    self.assertItemsEqual([111, 444], project_dict[123].owner_ids)
-    self.assertItemsEqual([222], project_dict[123].committer_ids)
-    self.assertItemsEqual([333], project_dict[123].contributor_ids)
+    six.assertCountEqual(self, [111, 444], project_dict[123].owner_ids)
+    six.assertCountEqual(self, [222], project_dict[123].committer_ids)
+    six.assertCountEqual(self, [333], project_dict[123].contributor_ids)
     self.assertEqual(234, project_dict[234].project_id)
-    self.assertItemsEqual([111], project_dict[234].owner_ids)
+    six.assertCountEqual(self, [111], project_dict[234].owner_ids)
     self.assertEqual(False, project_dict[123].issue_notify_always_detailed)
     self.assertEqual(True, project_dict[234].issue_notify_always_detailed)
 
@@ -278,7 +278,7 @@
     project_dict = self.project_service.GetProjects(
         self.cnxn, [123, 234])
     self.mox.VerifyAll()
-    self.assertItemsEqual([123, 234], list(project_dict.keys()))
+    six.assertCountEqual(self, [123, 234], list(project_dict.keys()))
     self.assertEqual('proj1', project_dict[123].project_name)
     self.assertEqual('proj2', project_dict[234].project_name)
 
@@ -288,7 +288,7 @@
     self.mox.ReplayAll()
     project_dict = self.project_service.GetProjects(self.cnxn, [234])
     self.mox.VerifyAll()
-    self.assertItemsEqual([234], list(project_dict.keys()))
+    six.assertCountEqual(self, [234], list(project_dict.keys()))
     self.assertEqual(
         [project_pb2.Project.ExtraPerms(
              member_id=111, perms=['FooPerm']),
@@ -297,7 +297,7 @@
         project_dict[234].extra_perms)
 
 
-  def testGetVisibleLiveProjects_AnyoneAccessWithUser(self):
+  def testGetVisibleProjects_AnyoneAccessWithUser(self):
     project_rows = [
         (
             234, 'proj2', 'test proj 2', 'test project', 'live', 'anyone', '',
@@ -311,13 +311,13 @@
     self.SetUpGetProjects()
     self.mox.ReplayAll()
     user_a = user_pb2.User(email='a@example.com')
-    project_ids = self.project_service.GetVisibleLiveProjects(
+    project_ids = self.project_service.GetVisibleProjects(
         self.cnxn, user_a, set([111]))
 
     self.mox.VerifyAll()
-    self.assertItemsEqual([234], project_ids)
+    six.assertCountEqual(self, [234], project_ids)
 
-  def testGetVisibleLiveProjects_AnyoneAccessWithAnon(self):
+  def testGetVisibleProjects_AnyoneAccessWithAnon(self):
     project_rows = [
         (
             234, 'proj2', 'test proj 2', 'test project', 'live', 'anyone', '',
@@ -330,13 +330,12 @@
         state=project_pb2.ProjectState.LIVE).AndReturn(project_rows)
     self.SetUpGetProjects()
     self.mox.ReplayAll()
-    project_ids = self.project_service.GetVisibleLiveProjects(
-        self.cnxn, None, None)
+    project_ids = self.project_service.GetVisibleProjects(self.cnxn, None, None)
 
     self.mox.VerifyAll()
-    self.assertItemsEqual([234], project_ids)
+    six.assertCountEqual(self, [234], project_ids)
 
-  def testGetVisibleLiveProjects_RestrictedAccessWithMember(self):
+  def testGetVisibleProjects_RestrictedAccessWithMember(self):
     project_rows = [
         (
             234, 'proj2', 'test proj 2', 'test project', 'live', 'members_only',
@@ -352,13 +351,13 @@
         state=project_pb2.ProjectState.LIVE).AndReturn(project_rows)
     self.mox.ReplayAll()
     user_a = user_pb2.User(email='a@example.com')
-    project_ids = self.project_service.GetVisibleLiveProjects(
+    project_ids = self.project_service.GetVisibleProjects(
         self.cnxn, user_a, set([111]))
 
     self.mox.VerifyAll()
-    self.assertItemsEqual([234], project_ids)
+    six.assertCountEqual(self, [234], project_ids)
 
-  def testGetVisibleLiveProjects_RestrictedAccessWithNonMember(self):
+  def testGetVisibleProjects_RestrictedAccessWithNonMember(self):
     project_rows = [
         (
             234, 'proj2', 'test proj 2', 'test project', 'live', 'members_only',
@@ -373,13 +372,13 @@
         state=project_pb2.ProjectState.LIVE).AndReturn(project_rows)
     self.mox.ReplayAll()
     user_a = user_pb2.User(email='a@example.com')
-    project_ids = self.project_service.GetVisibleLiveProjects(
+    project_ids = self.project_service.GetVisibleProjects(
         self.cnxn, user_a, set([111]))
 
     self.mox.VerifyAll()
-    self.assertItemsEqual([], project_ids)
+    six.assertCountEqual(self, [], project_ids)
 
-  def testGetVisibleLiveProjects_RestrictedAccessWithAnon(self):
+  def testGetVisibleProjects_RestrictedAccessWithAnon(self):
     project_rows = [
         (
             234, 'proj2', 'test proj 2', 'test project', 'live', 'members_only',
@@ -393,13 +392,12 @@
         self.cnxn, cols=['project_id'],
         state=project_pb2.ProjectState.LIVE).AndReturn(project_rows)
     self.mox.ReplayAll()
-    project_ids = self.project_service.GetVisibleLiveProjects(
-        self.cnxn, None, None)
+    project_ids = self.project_service.GetVisibleProjects(self.cnxn, None, None)
 
     self.mox.VerifyAll()
-    self.assertItemsEqual([], project_ids)
+    six.assertCountEqual(self, [], project_ids)
 
-  def testGetVisibleLiveProjects_RestrictedAccessWithSiteAdmin(self):
+  def testGetVisibleProjects_RestrictedAccessWithSiteAdmin(self):
     project_rows = [
         (
             234, 'proj2', 'test proj 2', 'test project', 'live', 'members_only',
@@ -415,13 +413,13 @@
     self.mox.ReplayAll()
     user_a = user_pb2.User(email='a@example.com')
     user_a.is_site_admin = True
-    project_ids = self.project_service.GetVisibleLiveProjects(
+    project_ids = self.project_service.GetVisibleProjects(
         self.cnxn, user_a, set([111]))
 
     self.mox.VerifyAll()
-    self.assertItemsEqual([234], project_ids)
+    six.assertCountEqual(self, [234], project_ids)
 
-  def testGetVisibleLiveProjects_ArchivedProject(self):
+  def testGetVisibleProjects_ArchivedProject(self):
     project_rows = [
         (
             234, 'proj2', 'test proj 2', 'test project', 'archived', 'anyone',
@@ -436,11 +434,11 @@
         state=project_pb2.ProjectState.LIVE).AndReturn(project_rows)
     self.mox.ReplayAll()
     user_a = user_pb2.User(email='a@example.com')
-    project_ids = self.project_service.GetVisibleLiveProjects(
+    project_ids = self.project_service.GetVisibleProjects(
         self.cnxn, user_a, set([111]))
 
     self.mox.VerifyAll()
-    self.assertItemsEqual([], project_ids)
+    six.assertCountEqual(self, [234], project_ids)
 
   def testGetProjectsByName(self):
     self.project_service.project_names_to_ids.CacheItem('proj1', 123)
@@ -451,7 +449,7 @@
     project_dict = self.project_service.GetProjectsByName(
         self.cnxn, ['proj1', 'proj2'])
     self.mox.VerifyAll()
-    self.assertItemsEqual(['proj1', 'proj2'], list(project_dict.keys()))
+    six.assertCountEqual(self, ['proj1', 'proj2'], list(project_dict.keys()))
     self.assertEqual(123, project_dict['proj1'].project_id)
     self.assertEqual(234, project_dict['proj2'].project_id)
 
@@ -584,18 +582,18 @@
         self.cnxn, {111, 888})
     owned_project_ids, membered_project_ids, contrib_project_ids = actual
     self.mox.VerifyAll()
-    self.assertItemsEqual([234], owned_project_ids)
-    self.assertItemsEqual([123], membered_project_ids)
-    self.assertItemsEqual([], contrib_project_ids)
+    six.assertCountEqual(self, [234], owned_project_ids)
+    six.assertCountEqual(self, [123], membered_project_ids)
+    six.assertCountEqual(self, [], contrib_project_ids)
 
   def testGetUserRolesInAllProjectsWithoutEffectiveIds(self):
     self.mox.ReplayAll()
     actual = self.project_service.GetUserRolesInAllProjects(self.cnxn, {})
     owned_project_ids, membered_project_ids, contrib_project_ids = actual
     self.mox.VerifyAll()
-    self.assertItemsEqual([], owned_project_ids)
-    self.assertItemsEqual([], membered_project_ids)
-    self.assertItemsEqual([], contrib_project_ids)
+    six.assertCountEqual(self, [], owned_project_ids)
+    six.assertCountEqual(self, [], membered_project_ids)
+    six.assertCountEqual(self, [], contrib_project_ids)
 
   def SetUpUpdateExtraPerms(self):
     self.project_service.extraperm_tbl.Delete(
diff --git a/services/test/service_manager_test.py b/services/test/service_manager_test.py
index 33c8706..e138c28 100644
--- a/services/test/service_manager_test.py
+++ b/services/test/service_manager_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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for the service_manager module."""
 from __future__ import print_function
diff --git a/services/test/spam_svc_test.py b/services/test/spam_svc_test.py
index 351ec62..156269c 100644
--- a/services/test/spam_svc_test.py
+++ b/services/test/spam_svc_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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for the spam service."""
 from __future__ import print_function
@@ -9,6 +8,8 @@
 from __future__ import absolute_import
 
 import mock
+import six
+import time
 import unittest
 
 try:
@@ -21,8 +22,9 @@
 import settings
 from framework import sql
 from framework import framework_constants
-from proto import user_pb2
-from proto import tracker_pb2
+from infra_libs import ts_mon
+from mrproto import user_pb2
+from mrproto import tracker_pb2
 from services import spam_svc
 from testing import fake
 from mock import Mock
@@ -51,6 +53,9 @@
 
     self.spam_service.report_tbl.Delete = Mock()
     self.spam_service.verdict_tbl.Delete = Mock()
+    self.now = int(time.time())
+
+    ts_mon.reset_for_unittest()
 
   def tearDown(self):
     self.testbed.deactivate()
@@ -84,7 +89,7 @@
     issue_reporters, comment_reporters = (
         self.spam_service.LookupIssueFlaggers(self.cnxn, 234))
     self.mox.VerifyAll()
-    self.assertItemsEqual([111], issue_reporters)
+    six.assertCountEqual(self, [111], issue_reporters)
     self.assertEqual({1: [222]}, comment_reporters)
 
   def testFlagIssues_overThresh(self):
@@ -96,7 +101,9 @@
         summary='sum',
         status='Live',
         issue_id=78901,
-        project_name='proj')
+        project_name='proj',
+        migration_modified_timestamp=1234567,
+        is_spam=False)
     issue.assume_stale = False  # We will store this issue.
 
     self.mock_report_tbl.InsertRows(self.cnxn,
@@ -118,6 +125,8 @@
         self.cnxn, self.issue_service, [issue], 111, True)
     self.mox.VerifyAll()
     self.assertIn(issue, self.issue_service.updated_issues)
+    self.assertEqual(issue.migration_modified_timestamp, self.now)
+    self.assertEqual(issue.is_spam, True)
 
     self.assertEqual(
         1,
@@ -137,7 +146,9 @@
         summary='sum',
         status='Live',
         issue_id=78901,
-        project_name='proj')
+        project_name='proj',
+        migration_modified_timestamp=1234567,
+        is_spam=False)
 
     self.mock_report_tbl.InsertRows(self.cnxn,
         ['issue_id', 'reported_user_id', 'user_id'],
@@ -157,6 +168,8 @@
     self.mox.VerifyAll()
 
     self.assertNotIn(issue, self.issue_service.updated_issues)
+    self.assertEqual(issue.migration_modified_timestamp, 1234567)
+    self.assertEqual(issue.is_spam, False)
     self.assertIsNone(
         self.spam_service.issue_actions.get(
             fields={
@@ -167,8 +180,15 @@
 
   def testUnflagIssue_overThresh(self):
     issue = fake.MakeTestIssue(
-        project_id=789, local_id=1, reporter_id=111, owner_id=456,
-        summary='sum', status='Live', issue_id=78901, is_spam=True)
+        project_id=789,
+        local_id=1,
+        reporter_id=111,
+        owner_id=456,
+        summary='sum',
+        status='Live',
+        issue_id=78901,
+        migration_modified_timestamp=1234567,
+        is_spam=True)
     self.mock_report_tbl.Delete(self.cnxn, issue_id=[issue.issue_id],
         comment_id=None, user_id=111)
     self.mock_report_tbl.Select(self.cnxn,
@@ -185,15 +205,23 @@
     self.mox.VerifyAll()
 
     self.assertNotIn(issue, self.issue_service.updated_issues)
-    self.assertEqual(True, issue.is_spam)
+    self.assertEqual(issue.migration_modified_timestamp, 1234567)
+    self.assertEqual(issue.is_spam, True)
 
   def testUnflagIssue_underThresh(self):
     """A non-member un-flagging an issue as spam should not be able
     to overturn the verdict to ham. This is different from previous
     behavior. See https://crbug.com/monorail/2232 for details."""
     issue = fake.MakeTestIssue(
-        project_id=789, local_id=1, reporter_id=111, owner_id=456,
-        summary='sum', status='Live', issue_id=78901, is_spam=True)
+        project_id=789,
+        local_id=1,
+        reporter_id=111,
+        owner_id=456,
+        summary='sum',
+        status='Live',
+        issue_id=78901,
+        migration_modified_timestamp=1234567,
+        is_spam=True)
     issue.assume_stale = False  # We will store this issue.
     self.mock_report_tbl.Delete(self.cnxn, issue_id=[issue.issue_id],
         comment_id=None, user_id=111)
@@ -211,12 +239,20 @@
     self.mox.VerifyAll()
 
     self.assertNotIn(issue, self.issue_service.updated_issues)
-    self.assertEqual(True, issue.is_spam)
+    self.assertEqual(issue.migration_modified_timestamp, 1234567)
+    self.assertEqual(issue.is_spam, True)
 
   def testUnflagIssue_underThreshNoManualOverride(self):
     issue = fake.MakeTestIssue(
-        project_id=789, local_id=1, reporter_id=111, owner_id=456,
-        summary='sum', status='Live', issue_id=78901, is_spam=True)
+        project_id=789,
+        local_id=1,
+        reporter_id=111,
+        owner_id=456,
+        summary='sum',
+        status='Live',
+        issue_id=78901,
+        migration_modified_timestamp=1234567,
+        is_spam=True)
     self.mock_report_tbl.Delete(self.cnxn, issue_id=[issue.issue_id],
         comment_id=None, user_id=111)
     self.mock_report_tbl.Select(self.cnxn,
@@ -234,7 +270,8 @@
     self.mox.VerifyAll()
 
     self.assertNotIn(issue, self.issue_service.updated_issues)
-    self.assertEqual(True, issue.is_spam)
+    self.assertEqual(issue.migration_modified_timestamp, 1234567)
+    self.assertEqual(issue.is_spam, True)
 
   def testIsExempt_RegularUser(self):
     author = user_pb2.MakeUser(111, email='test@example.com')
diff --git a/services/test/star_svc_test.py b/services/test/star_svc_test.py
index 3a5ce74..d3b4cea 100644
--- a/services/test/star_svc_test.py
+++ b/services/test/star_svc_test.py
@@ -1,13 +1,13 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for the star service."""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
+import six
 import unittest
 
 try:
@@ -15,13 +15,13 @@
 except ImportError:
   import mox
 import mock
+import time
 
 from google.appengine.ext import testbed
 
-import settings
 from mock import Mock
 from framework import sql
-from proto import user_pb2
+from services import service_manager
 from services import star_svc
 from testing import fake
 
@@ -78,13 +78,13 @@
     starrer_list_dict = self.star_service.LookupItemsStarrers(
         self.cnxn, [123, 234])
     self.mox.VerifyAll()
-    self.assertItemsEqual([123, 234], list(starrer_list_dict.keys()))
-    self.assertItemsEqual([111, 333], starrer_list_dict[123])
-    self.assertItemsEqual([111, 222], starrer_list_dict[234])
-    self.assertItemsEqual([111, 333],
-                          self.star_service.starrer_cache.GetItem(123))
-    self.assertItemsEqual([111, 222],
-                          self.star_service.starrer_cache.GetItem(234))
+    six.assertCountEqual(self, [123, 234], list(starrer_list_dict.keys()))
+    six.assertCountEqual(self, [111, 333], starrer_list_dict[123])
+    six.assertCountEqual(self, [111, 222], starrer_list_dict[234])
+    six.assertCountEqual(
+        self, [111, 333], self.star_service.starrer_cache.GetItem(123))
+    six.assertCountEqual(
+        self, [111, 222], self.star_service.starrer_cache.GetItem(234))
 
   def SetUpLookupStarredItemIDs(self):
     self.mock_tbl.Select(
@@ -96,9 +96,9 @@
     self.mox.ReplayAll()
     item_ids = self.star_service.LookupStarredItemIDs(self.cnxn, 111)
     self.mox.VerifyAll()
-    self.assertItemsEqual([123, 234], item_ids)
-    self.assertItemsEqual([123, 234],
-                          self.star_service.star_cache.GetItem(111))
+    six.assertCountEqual(self, [123, 234], item_ids)
+    six.assertCountEqual(
+        self, [123, 234], self.star_service.star_cache.GetItem(111))
 
   def testIsItemStarredBy(self):
     self.SetUpLookupStarredItemIDs()
@@ -129,7 +129,7 @@
     count_dict = self.star_service.CountItemsStars(
         self.cnxn, [123, 234])
     self.mox.VerifyAll()
-    self.assertItemsEqual([123, 234], list(count_dict.keys()))
+    six.assertCountEqual(self, [123, 234], list(count_dict.keys()))
     self.assertEqual(3, count_dict[123])
     self.assertEqual(2, count_dict[234])
 
@@ -189,17 +189,86 @@
 class IssueStarServiceTest(unittest.TestCase):
 
   def setUp(self):
-    self.mock_tbl = mock.Mock()
+    self.mox = mox.Mox()
+    self.mock_tbl = self.mox.CreateMock(sql.SQLTableManager)
     self.mock_tbl.Delete = mock.Mock()
     self.mock_tbl.InsertRows = mock.Mock()
 
+    self.mock_issue_tbl = self.mox.CreateMock(sql.SQLTableManager)
+
+    self.services = service_manager.Services()
+    self.services.issue = fake.IssueService()
+    self.services.config = fake.ConfigService()
+    self.services.features = fake.FeaturesService()
+
     self.cache_manager = fake.CacheManager()
     with mock.patch(
         'framework.sql.SQLTableManager', return_value=self.mock_tbl):
       self.issue_star = star_svc.IssueStarService(
           self.cache_manager)
+      self.issue_star.issue_tbl = self.mock_issue_tbl
 
     self.cnxn = 'fake connection'
+    self.now = int(time.time())
+
+  def testExpungeStarsByUsers(self):
+    self.mock_tbl.Select = mock.Mock(return_value=[(78901,), (78902,)])
+    self.mock_issue_tbl.Update = mock.Mock()
+
+    user_ids = [2, 3, 4]
+
+    self.mox.ReplayAll()
+    self.issue_star.ExpungeStarsByUsers(self.cnxn, user_ids, limit=40)
+    self.mox.VerifyAll()
+
+    self.mock_tbl.Select.assert_called_once_with(
+        self.cnxn,
+        cols=['IssueStar.issue_id'],
+        user_id=user_ids,
+        shard_id=mox.IgnoreArg(),
+        limit=40)
+    self.mock_tbl.Delete.assert_called_once_with(
+        self.cnxn, user_id=user_ids, commit=False, limit=40)
+    self.mock_issue_tbl.Update.assert_called_once_with(
+        self.cnxn, {'migration_modified': self.now},
+        id=[78901, 78902],
+        commit=False,
+        limit=40)
+
+  def testSetStarsBatch_Add(self):
+    issue = fake.MakeTestIssue(
+        project_id=789,
+        local_id=1,
+        reporter_id=111,
+        owner_id=456,
+        summary='sum',
+        status='Live',
+        issue_id=78901,
+        project_name='proj',
+        migration_modified_timestamp=1234567)
+    self.services.issue.TestAddIssue(issue)
+    config = self.services.config.GetProjectConfig(self.cnxn, 789)
+
+    # Set up mock for getting counts.
+    self.mock_tbl.Select(
+        self.cnxn,
+        cols=['issue_id', 'COUNT(user_id)'],
+        group_by=['issue_id'],
+        issue_id=[78901]).AndReturn([(78901, 2)])
+    self.mox.ReplayAll()
+
+    self.issue_star.SetStarsBatch(
+        self.cnxn, self.services, config, 78901, [111, 222], True)
+
+    self.mox.VerifyAll()
+    self.mock_tbl.InsertRows.assert_called_once_with(
+        self.cnxn, ['issue_id', 'user_id'], [(78901, 111), (78901, 222)],
+        ignore=True,
+        commit=True)
+
+    self.assertIn(issue, self.services.issue.updated_issues)
+    self.assertEqual(issue.migration_modified_timestamp, self.now)
+    self.assertEqual(issue.star_count, 2)
 
   def testSetStarsBatch_SkipIssueUpdate_Remove(self):
     self.issue_star.SetStarsBatch_SkipIssueUpdate(
diff --git a/services/test/template_svc_test.py b/services/test/template_svc_test.py
index 964722d..5e9f488 100644
--- a/services/test/template_svc_test.py
+++ b/services/test/template_svc_test.py
@@ -1,7 +1,6 @@
-# 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
+# Copyright 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for services.template_svc module."""
 from __future__ import print_function
@@ -13,7 +12,7 @@
 
 from mock import Mock, patch
 
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from services import template_svc
 from testing import fake
 from testing import testing_helpers
diff --git a/services/test/tracker_fulltext_test.py b/services/test/tracker_fulltext_test.py
index a4c935e..d977dea 100644
--- a/services/test/tracker_fulltext_test.py
+++ b/services/test/tracker_fulltext_test.py
@@ -1,13 +1,13 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for tracker_fulltext module."""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
+import six
 import unittest
 
 try:
@@ -19,8 +19,8 @@
 
 import settings
 from framework import framework_views
-from proto import ast_pb2
-from proto import tracker_pb2
+from mrproto import ast_pb2
+from mrproto import tracker_pb2
 from services import fulltext_helpers
 from services import tracker_fulltext
 from testing import fake
@@ -243,7 +243,7 @@
     issue_ids, capped = tracker_fulltext.SearchIssueFullText(
         [789], query_ast_conj, 1)
     self.mox.VerifyAll()
-    self.assertItemsEqual([123, 234], issue_ids)
+    six.assertCountEqual(self, [123, 234], issue_ids)
     self.assertFalse(capped)
 
   def testSearchIssueFullText_CrossProject(self):
@@ -262,7 +262,7 @@
     issue_ids, capped = tracker_fulltext.SearchIssueFullText(
         [789, 678], query_ast_conj, 1)
     self.mox.VerifyAll()
-    self.assertItemsEqual([123, 234], issue_ids)
+    six.assertCountEqual(self, [123, 234], issue_ids)
     self.assertFalse(capped)
 
   def testSearchIssueFullText_Capped(self):
@@ -280,7 +280,7 @@
       issue_ids, capped = tracker_fulltext.SearchIssueFullText(
           [789], query_ast_conj, 1)
       self.mox.VerifyAll()
-      self.assertItemsEqual([123, 234], issue_ids)
+      six.assertCountEqual(self, [123, 234], issue_ids)
       self.assertTrue(capped)
     finally:
       settings.fulltext_limit_per_shard = orig
diff --git a/services/test/user_svc_test.py b/services/test/user_svc_test.py
index 323d3eb..c709d75 100644
--- a/services/test/user_svc_test.py
+++ b/services/test/user_svc_test.py
@@ -1,13 +1,13 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for the user service."""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
+import six
 import unittest
 
 import mock
@@ -22,7 +22,7 @@
 from framework import exceptions
 from framework import framework_constants
 from framework import sql
-from proto import user_pb2
+from mrproto import user_pb2
 from services import user_svc
 from testing import fake
 
@@ -126,16 +126,17 @@
     self.mox.UnsetStubs()
     self.mox.ResetAll()
 
-  def SetUpCreateUsers(self):
+  def testCreateUsers(self):
+    self.user_service.user_tbl.Select(
+        self.cnxn,
+        cols=('user_id',),
+        user_id=[3035911623, 2996997680],
+    ).AndReturn([(2996997680,)])
     self.user_service.user_tbl.InsertRows(
         self.cnxn,
         ['user_id', 'email', 'obscure_email'],
-        [(3035911623, 'a@example.com', True),
-         (2996997680, 'b@example.com', True)]
+        [(3035911623, 'a@example.com', True)],
     ).AndReturn(None)
-
-  def testCreateUsers(self):
-    self.SetUpCreateUsers()
     self.mox.ReplayAll()
     self.user_service._CreateUsers(
         self.cnxn, ['a@example.com', 'b@example.com'])
@@ -461,7 +462,7 @@
     self.user_service.linkedaccount_tbl.Select.return_value = []
     with self.assertRaises(exceptions.InputException) as cm:
       self.user_service.AcceptLinkedChild(self.cnxn, 111, 333)
-    self.assertEqual('No such invite', cm.exception.message)
+    self.assertEqual('No such invite', str(cm.exception))
 
   def testAcceptLinkedChild_Normal(self):
     """Create linkage between accounts and remove invite."""
@@ -587,8 +588,8 @@
         self.cnxn, cols=['email'], limit=1000, offset=0,
         where=[('user_id != %s', [framework_constants.DELETED_USER_ID])],
         order_by=[('user_id ASC', [])])
-    self.assertItemsEqual(
-        emails, ['cow@test.com', 'pig@test.com', 'fox@test.com'])
+    six.assertCountEqual(
+        self, emails, ['cow@test.com', 'pig@test.com', 'fox@test.com'])
 
   def testGetAllUserEmailsBatch_CustomLimit(self):
     rows = [('cow@test.com',), ('pig@test.com',), ('fox@test.com',)]
@@ -599,5 +600,5 @@
         self.cnxn, cols=['email'], limit=30, offset=60,
         where=[('user_id != %s', [framework_constants.DELETED_USER_ID])],
         order_by=[('user_id ASC', [])])
-    self.assertItemsEqual(
-        emails, ['cow@test.com', 'pig@test.com', 'fox@test.com'])
+    six.assertCountEqual(
+        self, emails, ['cow@test.com', 'pig@test.com', 'fox@test.com'])
diff --git a/services/test/usergroup_svc_test.py b/services/test/usergroup_svc_test.py
index 10b2c8a..79b94d5 100644
--- a/services/test/usergroup_svc_test.py
+++ b/services/test/usergroup_svc_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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for the usergroup service."""
 from __future__ import print_function
@@ -10,6 +9,7 @@
 
 import collections
 import mock
+import six
 import unittest
 
 try:
@@ -22,7 +22,7 @@
 from framework import exceptions
 from framework import permissions
 from framework import sql
-from proto import usergroup_pb2
+from mrproto import usergroup_pb2
 from services import service_manager
 from services import usergroup_svc
 from testing import fake
@@ -49,9 +49,9 @@
     memberships_rows = [(111, 777), (111, 888), (222, 888)]
     actual = self.usergroup_service.memberships_2lc._DeserializeMemberships(
         memberships_rows)
-    self.assertItemsEqual([111, 222], list(actual.keys()))
-    self.assertItemsEqual([777, 888], actual[111])
-    self.assertItemsEqual([888], actual[222])
+    six.assertCountEqual(self, [111, 222], list(actual.keys()))
+    six.assertCountEqual(self, [777, 888], actual[111])
+    six.assertCountEqual(self, [888], actual[222])
 
 
 class UserGroupServiceTest(unittest.TestCase):
@@ -236,8 +236,8 @@
     members_dict, owners_dict = self.usergroup_service.LookupAllMembers(
         self.cnxn, [777])
     self.mox.VerifyAll()
-    self.assertItemsEqual([111, 222, 888, 999], members_dict[777])
-    self.assertItemsEqual([], owners_dict[777])
+    six.assertCountEqual(self, [111, 222, 888, 999], members_dict[777])
+    six.assertCountEqual(self, [], owners_dict[777])
 
   def testExpandAnyGroupEmailRecipients(self):
     self.usergroup_service.group_dag.initialized = True
@@ -257,8 +257,8 @@
     direct, indirect = self.usergroup_service.ExpandAnyGroupEmailRecipients(
         self.cnxn, [111, 777, 888, 999])
     self.mox.VerifyAll()
-    self.assertItemsEqual([111, 888, 999], direct)
-    self.assertItemsEqual([222, 444], indirect)
+    six.assertCountEqual(self, [111, 888, 999], direct)
+    six.assertCountEqual(self, [222, 444], indirect)
 
   def SetUpLookupMembers(self, group_member_dict):
     mock_membership_rows = []
@@ -275,7 +275,7 @@
     self.mox.ReplayAll()
     member_ids, _ = self.usergroup_service.LookupMembers(self.cnxn, [])
     self.mox.VerifyAll()
-    self.assertItemsEqual({}, member_ids)
+    six.assertCountEqual(self, {}, member_ids)
 
   def testLookupMembers_Nonexistent(self):
     """If some requested groups don't exist, they are ignored."""
@@ -283,7 +283,7 @@
     self.mox.ReplayAll()
     member_ids, _ = self.usergroup_service.LookupMembers(self.cnxn, [777])
     self.mox.VerifyAll()
-    self.assertItemsEqual([], member_ids[777])
+    six.assertCountEqual(self, [], member_ids[777])
 
   def testLookupMembers_AllEmpty(self):
     """Requesting all empty groups results in no members."""
@@ -291,14 +291,14 @@
     self.mox.ReplayAll()
     member_ids, _ = self.usergroup_service.LookupMembers(self.cnxn, [888, 999])
     self.mox.VerifyAll()
-    self.assertItemsEqual([], member_ids[888])
+    six.assertCountEqual(self, [], member_ids[888])
 
   def testLookupMembers_OneGroup(self):
     self.SetUpLookupMembers({888: [111, 222]})
     self.mox.ReplayAll()
     member_ids, _ = self.usergroup_service.LookupMembers(self.cnxn, [888])
     self.mox.VerifyAll()
-    self.assertItemsEqual([111, 222], member_ids[888])
+    six.assertCountEqual(self, [111, 222], member_ids[888])
 
   def testLookupMembers_GroupsAndNonGroups(self):
     """We ignore any non-groups passed in."""
@@ -307,7 +307,7 @@
     member_ids, _ = self.usergroup_service.LookupMembers(
         self.cnxn, [111, 333, 888])
     self.mox.VerifyAll()
-    self.assertItemsEqual([111, 222], member_ids[888])
+    six.assertCountEqual(self, [111, 222], member_ids[888])
 
   def testLookupMembers_OverlappingGroups(self):
     """We get the union of IDs.  Imagine 888 = {111} and 999 = {111, 222}."""
@@ -315,8 +315,8 @@
     self.mox.ReplayAll()
     member_ids, _ = self.usergroup_service.LookupMembers(self.cnxn, [888, 999])
     self.mox.VerifyAll()
-    self.assertItemsEqual([111, 222], member_ids[999])
-    self.assertItemsEqual([111], member_ids[888])
+    six.assertCountEqual(self, [111, 222], member_ids[999])
+    six.assertCountEqual(self, [111], member_ids[888])
 
   def testLookupVisibleMembers_LimitedVisiblity(self):
     """We get only the member IDs in groups that the user is allowed to see."""
@@ -332,7 +332,7 @@
         self.cnxn, [888, 999], permissions.USER_PERMISSIONSET, set(),
         self.services)
     self.mox.VerifyAll()
-    self.assertItemsEqual([111], member_ids[888])
+    six.assertCountEqual(self, [111], member_ids[888])
     self.assertNotIn(999, member_ids)
 
   def SetUpGetAllUserGroupsInfo(self, mock_settings_rows, mock_count_rows,