Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/api/test/projects_servicer_test.py b/api/test/projects_servicer_test.py
new file mode 100644
index 0000000..b3084c3
--- /dev/null
+++ b/api/test/projects_servicer_test.py
@@ -0,0 +1,1086 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the projects servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+from mock import patch
+
+from components.prpc import codes
+from components.prpc import context
+from components.prpc import server
+
+from api import projects_servicer
+from api.api_proto import common_pb2
+from api.api_proto import issue_objects_pb2
+from api.api_proto import project_objects_pb2
+from api.api_proto import projects_pb2
+from framework import authdata
+from framework import exceptions
+from framework import framework_constants
+from framework import monorailcontext
+from framework import permissions
+from proto import tracker_pb2
+from proto import project_pb2
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+from testing import fake
+from testing import testing_helpers
+from services import service_manager
+
+
+class ProjectsServicerTest(unittest.TestCase):
+
+  def setUp(self):
+    self.cnxn = fake.MonorailConnection()
+    self.services = service_manager.Services(
+        config=fake.ConfigService(),
+        issue=fake.IssueService(),
+        user=fake.UserService(),
+        usergroup=fake.UserGroupService(),
+        project=fake.ProjectService(),
+        project_star=fake.ProjectStarService(),
+        features=fake.FeaturesService())
+
+    self.admin = self.services.user.TestAddUser('admin@example.com', 123)
+    self.admin.is_site_admin = True
+    self.owner = self.services.user.TestAddUser('owner@example.com', 111)
+    self.services.user.TestAddUser('user_222@example.com', 222)
+    self.services.user.TestAddUser('user_333@example.com', 333)
+    self.services.user.TestAddUser('user_444@example.com', 444)
+    self.services.user.TestAddUser('user_666@example.com', 666)
+
+    # User group 888 has members: user_555 and proj@monorail.com
+    self.services.user.TestAddUser('group888@googlegroups.com', 888)
+    self.services.usergroup.TestAddGroupSettings(
+        888, 'group888@googlegroups.com')
+    self.services.usergroup.TestAddMembers(888, [555, 1001])
+
+    # User group 999 has members: user_111 and user_444
+    self.services.user.TestAddUser('group999@googlegroups.com', 999)
+    self.services.usergroup.TestAddGroupSettings(
+        999, 'group999@googlegroups.com')
+    self.services.usergroup.TestAddMembers(999, [111, 444])
+
+    # User group 777 has members: user_666 and group 999.
+    self.services.user.TestAddUser('group777@googlegroups.com', 777)
+    self.services.usergroup.TestAddGroupSettings(
+        777, 'group777@googlegroups.com')
+    self.services.usergroup.TestAddMembers(777, [666, 999])
+
+    self.project = self.services.project.TestAddProject(
+        'proj',
+        project_id=789,
+        owner_ids=[111],
+        committer_ids=[222],
+        contrib_ids=[333])
+    self.projects_svcr = projects_servicer.ProjectsServicer(
+        self.services, make_rate_limiter=False)
+    self.prpc_context = context.ServicerContext()
+    self.prpc_context.set_code(codes.StatusCode.OK)
+
+  def CallWrapped(self, wrapped_handler, *args, **kwargs):
+    return wrapped_handler.wrapped(self.projects_svcr, *args, **kwargs)
+
+  def testListProjects_Normal(self):
+    """We can get a list of all projects on the site."""
+    request = projects_pb2.ListProjectsRequest()
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(self.projects_svcr.ListProjects, mc, request)
+    self.assertEqual(2, len(response.projects))
+
+  def testGetConfig_Normal(self):
+    """We can get a project config."""
+    request = projects_pb2.GetConfigRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(self.projects_svcr.GetConfig, mc, request)
+    self.assertEqual('proj', response.project_name)
+
+  def testGetConfig_NoSuchProject(self):
+    """We reject a request to get a config for a non-existent project."""
+    request = projects_pb2.GetConfigRequest(project_name='unknown-proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    with self.assertRaises(exceptions.NoSuchProjectException):
+      self.CallWrapped(self.projects_svcr.GetConfig, mc, request)
+
+  def testGetConfig_PermissionDenied(self):
+    """We reject a request to get a config for a non-viewable project."""
+    self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+    request = projects_pb2.GetConfigRequest(project_name='proj')
+
+    # User is a member of the members-only project.
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(self.projects_svcr.GetConfig, mc, request)
+    self.assertEqual('proj', response.project_name)
+
+    # User is not a member of the members-only project.
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+    with self.assertRaises(permissions.PermissionException):
+      self.CallWrapped(self.projects_svcr.GetConfig, mc, request)
+
+  @patch('businesslogic.work_env.WorkEnv.ListProjectTemplates')
+  def testListProjectTemplates_Normal(self, mockListProjectTemplates):
+    fd_1 = tracker_pb2.FieldDef(
+        field_name='FirstField', field_id=1,
+        field_type=tracker_pb2.FieldTypes.STR_TYPE)
+    fd_2 = tracker_pb2.FieldDef(
+        field_name='LegalApproval', field_id=2,
+        field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE)
+    component = tracker_pb2.ComponentDef(component_id=1, path='dude')
+    status_def = tracker_pb2.StatusDef(status='New', means_open=True)
+    config = tracker_pb2.ProjectIssueConfig(
+        project_id=789, field_defs=[fd_1, fd_2], component_defs=[component],
+        well_known_statuses=[status_def])
+    self.services.config.StoreConfig(self.cnxn, config)
+    admin1 = self.services.user.TestAddUser('admin@example.com', 222)
+    appr1 = self.services.user.TestAddUser('approver@example.com', 333)
+    setter = self.services.user.TestAddUser('setter@example.com', 444)
+    template = tracker_pb2.TemplateDef(
+        name='Chicken', content='description', summary='summary',
+        status='New', admin_ids=[admin1.user_id],
+        field_values=[tracker_bizobj.MakeFieldValue(
+            fd_1.field_id, None, 'Cow', None, None, None, False)],
+        component_ids=[component.component_id],
+        approval_values=[tracker_pb2.ApprovalValue(
+            approval_id=2, approver_ids=[appr1.user_id],
+            setter_id=setter.user_id)])
+    mockListProjectTemplates.return_value = [template]
+
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    request = projects_pb2.ListProjectTemplatesRequest(project_name='proj')
+    response = self.CallWrapped(
+        self.projects_svcr.ListProjectTemplates, mc, request)
+    self.assertEqual(
+        response,
+        projects_pb2.ListProjectTemplatesResponse(
+            templates=[project_objects_pb2.TemplateDef(
+                template_name='Chicken',
+                content='description',
+                summary='summary',
+                status_ref=common_pb2.StatusRef(
+                    status='New',
+                    is_derived=False,
+                    means_open=True),
+                owner_defaults_to_member=True,
+                admin_refs=[
+                  common_pb2.UserRef(
+                      user_id=admin1.user_id,
+                      display_name=testing_helpers.ObscuredEmail(admin1.email),
+                      is_derived=False)],
+                field_values=[
+                  issue_objects_pb2.FieldValue(
+                    field_ref=common_pb2.FieldRef(
+                        field_id=fd_1.field_id,
+                        field_name=fd_1.field_name,
+                        type=common_pb2.STR_TYPE),
+                    value='Cow')],
+                component_refs=[
+                    common_pb2.ComponentRef(
+                        path=component.path, is_derived=False)],
+                approval_values=[
+                  issue_objects_pb2.Approval(
+                    field_ref=common_pb2.FieldRef(
+                        field_id=fd_2.field_id,
+                        field_name=fd_2.field_name,
+                        type=common_pb2.APPROVAL_TYPE),
+                    setter_ref=common_pb2.UserRef(
+                        user_id=setter.user_id,
+                        display_name=testing_helpers.ObscuredEmail(
+                            setter.email)),
+                    phase_ref=issue_objects_pb2.PhaseRef(),
+                    approver_refs=[common_pb2.UserRef(
+                        user_id=appr1.user_id,
+                        display_name=testing_helpers.ObscuredEmail(appr1.email),
+                        is_derived=False)])],
+          )]))
+
+  def testListProjectTemplates_NoProjectName(self):
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    request = projects_pb2.ListProjectTemplatesRequest()
+    with self.assertRaises(exceptions.InputException):
+      self.CallWrapped(self.projects_svcr.ListProjectTemplates, mc, request)
+
+  def testListProjectTemplates_NoSuchProject(self):
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    request = projects_pb2.ListProjectTemplatesRequest(project_name='ghost')
+    with self.assertRaises(exceptions.NoSuchProjectException):
+      self.CallWrapped(self.projects_svcr.ListProjectTemplates, mc, request)
+
+  def testListProjectTemplates_PermissionDenied(self):
+    self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+    request = projects_pb2.GetConfigRequest(project_name='proj')
+    with self.assertRaises(permissions.PermissionException):
+      self.CallWrapped(self.projects_svcr.ListProjectTemplates, mc, request)
+
+  def testGetPresentationConfig_Normal(self):
+    """Test getting project summary, thumbnail url, custom issue entry, etc."""
+    config = tracker_pb2.ProjectIssueConfig(project_id=789)
+    self.project.summary = 'project summary'
+    config.custom_issue_entry_url = 'issue entry url'
+    config.member_default_query = 'default query'
+    config.default_col_spec = 'ID Summary'
+    config.default_sort_spec = 'Priority Status'
+    config.default_x_attr = 'Priority'
+    config.default_y_attr = 'Status'
+    self.project.revision_url_format = 'revision url format'
+    self.services.config.StoreConfig(self.cnxn, config)
+
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+    request = projects_pb2.GetPresentationConfigRequest(project_name='proj')
+    response = self.CallWrapped(
+        self.projects_svcr.GetPresentationConfig, mc, request)
+
+    self.assertEqual('project summary', response.project_summary)
+    self.assertEqual('issue entry url', response.custom_issue_entry_url)
+    self.assertEqual('default query', response.default_query)
+    self.assertEqual('ID Summary', response.default_col_spec)
+    self.assertEqual('Priority Status', response.default_sort_spec)
+    self.assertEqual('Priority', response.default_x_attr)
+    self.assertEqual('Status', response.default_y_attr)
+    self.assertEqual('revision url format', response.revision_url_format)
+
+  def testGetPresentationConfig_SavedQueriesAllowed(self):
+    """Only project members or higher can see project saved queries."""
+    self.services.features.UpdateCannedQueries(self.cnxn, 789, [
+        tracker_pb2.SavedQuery(query_id=101, name='test', query='owner:me'),
+        tracker_pb2.SavedQuery(query_id=202, name='hello', query='world')
+    ])
+
+    # User 333 is a contributor.
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='user_333@example.com')
+
+    request = projects_pb2.GetPresentationConfigRequest(project_name='proj')
+    response = self.CallWrapped(self.projects_svcr.GetPresentationConfig, mc,
+        request)
+
+    self.assertEqual(2, len(response.saved_queries))
+
+    self.assertEqual(101, response.saved_queries[0].query_id)
+    self.assertEqual('test', response.saved_queries[0].name)
+    self.assertEqual('owner:me', response.saved_queries[0].query)
+
+    self.assertEqual(202, response.saved_queries[1].query_id)
+    self.assertEqual('hello', response.saved_queries[1].name)
+    self.assertEqual('world', response.saved_queries[1].query)
+
+  def testGetPresentationConfig_SavedQueriesDenied(self):
+    """Only project members or higher can see project saved queries."""
+    self.services.features.UpdateCannedQueries(self.cnxn, 789, [
+        tracker_pb2.SavedQuery(query_id=101, name='test', query='owner:me'),
+        tracker_pb2.SavedQuery(query_id=202, name='hello', query='world')
+    ])
+
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+
+    request = projects_pb2.GetPresentationConfigRequest(project_name='proj')
+    response = self.CallWrapped(self.projects_svcr.GetPresentationConfig, mc,
+        request)
+
+    self.assertEqual(0, len(response.saved_queries))
+
+  def testGetCustomPermissions_Normal(self):
+    self.project.extra_perms = [
+        project_pb2.Project.ExtraPerms(
+            member_id=111,
+            perms=['FooPerm', 'BarPerm'])]
+
+    request = projects_pb2.GetConfigRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='foo@example.org')
+    response = self.CallWrapped(
+        self.projects_svcr.GetCustomPermissions, mc, request)
+    self.assertEqual(['BarPerm', 'FooPerm'], response.permissions)
+
+  def testGetCustomPermissions_PermissionsAreDedupped(self):
+    self.project.extra_perms = [
+        project_pb2.Project.ExtraPerms(
+            member_id=111,
+            perms=['FooPerm', 'FooPerm']),
+        project_pb2.Project.ExtraPerms(
+            member_id=222,
+            perms=['FooPerm'])]
+
+    request = projects_pb2.GetConfigRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='foo@example.org')
+    response = self.CallWrapped(
+        self.projects_svcr.GetCustomPermissions, mc, request)
+    self.assertEqual(['FooPerm'], response.permissions)
+
+  def testGetCustomPermissions_PermissionsAreSorted(self):
+    self.project.extra_perms = [
+        project_pb2.Project.ExtraPerms(
+            member_id=111,
+            perms=['FooPerm', 'BarPerm']),
+        project_pb2.Project.ExtraPerms(
+            member_id=222,
+            perms=['BazPerm'])]
+
+    request = projects_pb2.GetConfigRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='foo@example.org')
+    response = self.CallWrapped(
+        self.projects_svcr.GetCustomPermissions, mc, request)
+    self.assertEqual(['BarPerm', 'BazPerm', 'FooPerm'], response.permissions)
+
+  def testGetCustomPermissions_IgnoreStandardPermissions(self):
+    self.project.extra_perms = [
+        project_pb2.Project.ExtraPerms(
+            member_id=111,
+            perms=permissions.STANDARD_PERMISSIONS + ['FooPerm'])]
+
+    request = projects_pb2.GetConfigRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='foo@example.org')
+    response = self.CallWrapped(
+        self.projects_svcr.GetCustomPermissions, mc, request)
+    self.assertEqual(['FooPerm'], response.permissions)
+
+  def testGetCustomPermissions_NoCustomPermissions(self):
+    self.project.extra_perms = []
+    request = projects_pb2.GetConfigRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='foo@example.org')
+    response = self.CallWrapped(
+        self.projects_svcr.GetCustomPermissions, mc, request)
+    self.assertEqual([], response.permissions)
+
+  def assertVisibleMembers(self, expected_user_ids, expected_group_ids,
+                           requester=None):
+    request = projects_pb2.GetVisibleMembersRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester=requester)
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(
+        self.projects_svcr.GetVisibleMembers, mc, request)
+    self.assertEqual(
+        expected_user_ids,
+        [user_ref.user_id for user_ref in response.user_refs])
+    # Assert that we get the full email address.
+    self.assertEqual(
+        [self.services.user.LookupUserEmail(self.cnxn, user_id)
+         for user_id in expected_user_ids],
+        [user_ref.display_name for user_ref in response.user_refs])
+    self.assertEqual(
+        expected_group_ids,
+        [group_ref.user_id for group_ref in response.group_refs])
+    # Assert that we get the full email address.
+    self.assertEqual(
+        [self.services.user.LookupUserEmail(self.cnxn, user_id)
+         for user_id in expected_group_ids],
+        [group_ref.display_name for group_ref in response.group_refs])
+    return response
+
+  def testGetVisibleMembers_Normal(self):
+    # Not logged in - Test users have their email addresses obscured to
+    # non-project members by default.
+    self.assertVisibleMembers([], [])
+    # Logged in as non project member
+    self.assertVisibleMembers([], [], requester='foo@example.com')
+    # Logged in as owner
+    self.assertVisibleMembers([111, 222, 333], [],
+                              requester='owner@example.com')
+    # Logged in as committer
+    self.assertVisibleMembers([111, 222, 333], [],
+                              requester='user_222@example.com')
+    # Logged in as contributor
+    self.assertVisibleMembers([111, 222, 333], [],
+                              requester='user_333@example.com')
+
+  def testGetVisibleMembers_OnlyOwnersSeeContributors(self):
+    self.project.only_owners_see_contributors = True
+    # Not logged in
+    with self.assertRaises(permissions.PermissionException):
+      self.assertVisibleMembers([111, 222], [])
+    # Logged in with a non-member
+    with self.assertRaises(permissions.PermissionException):
+      self.assertVisibleMembers([111, 222], [], requester='foo@example.com')
+    # Logged in as owner
+    self.assertVisibleMembers([111, 222, 333], [],
+                              requester='owner@example.com')
+    # Logged in as committer
+    self.assertVisibleMembers([111, 222, 333], [],
+                              requester='user_222@example.com')
+    # Logged in as contributor
+    with self.assertRaises(permissions.PermissionException):
+      self.assertVisibleMembers(
+          [111, 222], [], requester='user_333@example.com')
+
+  def testGetVisibleMembers_MemberIsGroup(self):
+    self.project.contributor_ids.extend([999])
+    self.assertVisibleMembers([999, 111, 222, 333, 444], [999],
+                              requester='owner@example.com')
+
+  def testGetVisibleMembers_AcExclusion(self):
+    self.services.project.ac_exclusion_ids[self.project.project_id] = [333]
+    self.assertVisibleMembers([111, 222], [], requester='owner@example.com')
+
+  def testGetVisibleMembers_NoExpand(self):
+    self.services.project.no_expand_ids[self.project.project_id] = [999]
+    self.project.contributor_ids.extend([999])
+    self.assertVisibleMembers([999, 111, 222, 333], [999],
+                              requester='owner@example.com')
+
+  def testGetVisibleMembers_ObscuredEmails(self):
+    # Unobscure the owner's email. Non-project members can see.
+    self.services.user.UpdateUserSettings(
+        self.cnxn, 111, self.owner, obscure_email=False)
+
+    # Not logged in
+    self.assertVisibleMembers([111], [])
+    # Logged in as not a project member
+    self.assertVisibleMembers([111], [], requester='foo@example.com')
+    # Logged in as owner
+    self.assertVisibleMembers(
+        [111, 222, 333], [], requester='owner@example.com')
+    # Logged in as committer
+    self.assertVisibleMembers(
+        [111, 222, 333], [], requester='user_222@example.com')
+    # Logged in as contributor
+    self.assertVisibleMembers(
+        [111, 222, 333], [], requester='user_333@example.com')
+
+  def testListStatuses(self):
+    request = projects_pb2.ListStatusesRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.ListStatuses, mc, request)
+    self.assertFalse(response.restrict_to_known)
+    self.assertEqual(
+        [('New', True),
+         ('Accepted', True),
+         ('Started', True),
+         ('Fixed', False),
+         ('Verified', False),
+         ('Invalid', False),
+         ('Duplicate', False),
+         ('WontFix', False),
+         ('Done', False)],
+        [(status_def.status, status_def.means_open)
+         for status_def in response.status_defs])
+    self.assertEqual(
+        [('Duplicate', False)],
+        [(status_def.status, status_def.means_open)
+         for status_def in response.statuses_offer_merge])
+
+  def testListComponents(self):
+    self.services.config.CreateComponentDef(
+        self.cnxn, self.project.project_id, 'Foo', 'Foo Component', True, [],
+        [], True, 111, [])
+    self.services.config.CreateComponentDef(
+        self.cnxn, self.project.project_id, 'Bar', 'Bar Component', False, [],
+        [], True, 111, [])
+    self.services.config.CreateComponentDef(
+        self.cnxn, self.project.project_id, 'Bar>Baz', 'Baz Component',
+        False, [], [], True, 111, [])
+
+    request = projects_pb2.ListComponentsRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.ListComponents, mc, request)
+
+    self.assertEqual(
+        [project_objects_pb2.ComponentDef(
+            path='Foo',
+            docstring='Foo Component',
+            deprecated=True),
+         project_objects_pb2.ComponentDef(
+             path='Bar',
+             docstring='Bar Component',
+             deprecated=False),
+         project_objects_pb2.ComponentDef(
+             path='Bar>Baz',
+             docstring='Baz Component',
+             deprecated=False)],
+        list(response.component_defs))
+
+  def testListComponents_IncludeAdminInfo(self):
+    self.services.config.CreateComponentDef(
+        self.cnxn, self.project.project_id, 'Foo', 'Foo Component', True, [],
+        [], 1234567, 111, [])
+    self.services.config.CreateComponentDef(
+        self.cnxn, self.project.project_id, 'Bar', 'Bar Component', False, [],
+        [], 1234568, 111, [])
+    self.services.config.CreateComponentDef(
+        self.cnxn, self.project.project_id, 'Bar>Baz', 'Baz Component',
+        False, [], [], 1234569, 111, [])
+    creator_ref = common_pb2.UserRef(
+        user_id=111,
+        display_name='owner@example.com')
+
+    request = projects_pb2.ListComponentsRequest(
+        project_name='proj', include_admin_info=True)
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.ListComponents, mc, request)
+
+    self.assertEqual(
+        [project_objects_pb2.ComponentDef(
+            path='Foo',
+            docstring='Foo Component',
+            deprecated=True,
+            created=1234567,
+            creator_ref=creator_ref),
+         project_objects_pb2.ComponentDef(
+             path='Bar',
+             docstring='Bar Component',
+             deprecated=False,
+             created=1234568,
+             creator_ref=creator_ref),
+         project_objects_pb2.ComponentDef(
+             path='Bar>Baz',
+             docstring='Baz Component',
+             deprecated=False,
+             created=1234569,
+             creator_ref=creator_ref),
+            ],
+        list(response.component_defs))
+
+  def AddField(self, name, **kwargs):
+    if kwargs.get('needs_perm'):
+      kwargs['needs_member'] = True
+    kwargs.setdefault('cnxn', self.cnxn)
+    kwargs.setdefault('project_id', self.project.project_id)
+    kwargs.setdefault('field_name', name)
+    kwargs.setdefault('field_type_str', 'USER_TYPE')
+    for arg in ('applic_type', 'applic_pred', 'is_required', 'is_niche',
+                'is_multivalued', 'min_value', 'max_value', 'regex',
+                'needs_member', 'needs_perm', 'grants_perm', 'notify_on',
+                'date_action_str', 'docstring'):
+      kwargs.setdefault(arg, None)
+    for arg in ('admin_ids', 'editor_ids'):
+      kwargs.setdefault(arg, [])
+
+    self.services.config.CreateFieldDef(**kwargs)
+
+  def testListFields_Normal(self):
+    self.AddField('Foo Field', needs_perm=permissions.EDIT_ISSUE)
+
+    request = projects_pb2.ListFieldsRequest(
+        project_name='proj', include_user_choices=True)
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.ListFields, mc, request)
+
+    self.assertEqual(1, len(response.field_defs))
+    field = response.field_defs[0]
+    self.assertEqual('Foo Field', field.field_ref.field_name)
+    self.assertEqual(
+        [111, 222],
+        sorted([user_ref.user_id for user_ref in field.user_choices]))
+    self.assertEqual(
+        ['owner@example.com', 'user_222@example.com'],
+        sorted([user_ref.display_name for user_ref in field.user_choices]))
+
+  def testListFields_DontIncludeUserChoices(self):
+    self.AddField('Foo Field', needs_perm=permissions.EDIT_ISSUE)
+
+    request = projects_pb2.ListFieldsRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.ListFields, mc, request)
+
+    self.assertEqual(1, len(response.field_defs))
+    field = response.field_defs[0]
+    self.assertEqual(0, len(field.user_choices))
+
+  def testListFields_IncludeAdminInfo(self):
+    self.AddField('Foo Field', needs_perm=permissions.EDIT_ISSUE, is_niche=True,
+                  applic_type='Foo Applic Type')
+
+    request = projects_pb2.ListFieldsRequest(
+        project_name='proj', include_admin_info=True)
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.ListFields, mc, request)
+
+    self.assertEqual(1, len(response.field_defs))
+    field = response.field_defs[0]
+    self.assertEqual('Foo Field', field.field_ref.field_name)
+    self.assertEqual(True, field.is_niche)
+    self.assertEqual('Foo Applic Type', field.applicable_type)
+
+  def testListFields_EnumFieldChoices(self):
+    self.AddField('Type', field_type_str='ENUM_TYPE')
+
+    request = projects_pb2.ListFieldsRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.ListFields, mc, request)
+
+    self.assertEqual(1, len(response.field_defs))
+    field = response.field_defs[0]
+    self.assertEqual('Type', field.field_ref.field_name)
+    self.assertEqual(
+        ['Defect', 'Enhancement', 'Task', 'Other'],
+        [label.label for label in field.enum_choices])
+
+  def testListFields_CustomPermission(self):
+    self.AddField('Foo Field', needs_perm='FooPerm')
+    self.project.extra_perms = [
+        project_pb2.Project.ExtraPerms(
+            member_id=111,
+            perms=['UnrelatedPerm']),
+        project_pb2.Project.ExtraPerms(
+            member_id=222,
+            perms=['FooPerm'])]
+
+    request = projects_pb2.ListFieldsRequest(
+        project_name='proj', include_user_choices=True)
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.ListFields, mc, request)
+
+    self.assertEqual(1, len(response.field_defs))
+    field = response.field_defs[0]
+    self.assertEqual('Foo Field', field.field_ref.field_name)
+    self.assertEqual(
+        [222],
+        sorted([user_ref.user_id for user_ref in field.user_choices]))
+    self.assertEqual(
+        ['user_222@example.com'],
+        sorted([user_ref.display_name for user_ref in field.user_choices]))
+
+  def testListFields_IndirectPermission(self):
+    """Test that the permissions of effective ids are also considered."""
+    self.AddField('Foo Field', needs_perm='FooPerm')
+    self.project.contributor_ids.extend([999])
+    self.project.extra_perms = [
+        project_pb2.Project.ExtraPerms(
+            member_id=999,
+            perms=['FooPerm', 'BarPerm'])]
+
+    request = projects_pb2.ListFieldsRequest(
+        project_name='proj', include_user_choices=True)
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(
+        self.projects_svcr.ListFields, mc, request)
+
+    self.assertEqual(1, len(response.field_defs))
+    field = response.field_defs[0]
+    self.assertEqual('Foo Field', field.field_ref.field_name)
+    # Users 111 and 444 are members of group 999, which has the needed
+    # permission.
+    self.assertEqual(
+        [111, 444, 999],
+        sorted([user_ref.user_id for user_ref in field.user_choices]))
+    self.assertEqual(
+        ['group999@googlegroups.com', 'owner@example.com',
+         'user_444@example.com'],
+        sorted([user_ref.display_name for user_ref in field.user_choices]))
+
+  def testListFields_TwiceIndirectPermission(self):
+    """Test that only direct memberships are considered."""
+    self.AddField('Foo Field', needs_perm='FooPerm')
+    # User group 777 has members: user_666 and group 999.
+    self.project.contributor_ids.extend([777])
+    self.project.contributor_ids.extend([999])
+    self.project.extra_perms = [
+        project_pb2.Project.ExtraPerms(
+            member_id=777, perms=['FooPerm', 'BarPerm'])
+    ]
+
+    request = projects_pb2.ListFieldsRequest(
+        project_name='proj', include_user_choices=True)
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(self.projects_svcr.ListFields, mc, request)
+
+    self.assertEqual(1, len(response.field_defs))
+    field = response.field_defs[0]
+    self.assertEqual('Foo Field', field.field_ref.field_name)
+    self.assertEqual(
+        [666, 777, 999],
+        sorted([user_ref.user_id for user_ref in field.user_choices]))
+    self.assertEqual(
+        [
+            'group777@googlegroups.com', 'group999@googlegroups.com',
+            'user_666@example.com'
+        ], sorted([user_ref.display_name for user_ref in field.user_choices]))
+
+  def testListFields_NoPermissionsNeeded(self):
+    self.AddField('Foo Field')
+
+    request = projects_pb2.ListFieldsRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.ListFields, mc, request)
+
+    self.assertEqual(1, len(response.field_defs))
+    field = response.field_defs[0]
+    self.assertEqual('Foo Field', field.field_ref.field_name)
+
+  def testListFields_MultipleFields(self):
+    self.AddField('Bar Field', needs_perm=permissions.VIEW)
+    self.AddField('Foo Field', needs_perm=permissions.EDIT_ISSUE)
+
+    request = projects_pb2.ListFieldsRequest(
+        project_name='proj', include_user_choices=True)
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.ListFields, mc, request)
+
+    self.assertEqual(2, len(response.field_defs))
+    field_defs = sorted(
+        response.field_defs, key=lambda field: field.field_ref.field_name)
+
+    self.assertEqual(
+        ['Bar Field', 'Foo Field'],
+        [field.field_ref.field_name for field in field_defs])
+    self.assertEqual(
+        [[111, 222, 333],
+         [111, 222]],
+        [sorted(user_ref.user_id for user_ref in field.user_choices)
+         for field in field_defs])
+    self.assertEqual(
+        [['owner@example.com', 'user_222@example.com', 'user_333@example.com'],
+         ['owner@example.com', 'user_222@example.com']],
+        [sorted(user_ref.display_name for user_ref in field.user_choices)
+         for field in field_defs])
+
+  def testListFields_NoFields(self):
+    request = projects_pb2.ListFieldsRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.ListFields, mc, request)
+
+    self.assertEqual(0, len(response.field_defs))
+
+  def testGetLabelOptions_Normal(self):
+    request = projects_pb2.GetLabelOptionsRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.GetLabelOptions, mc, request)
+
+    expected_label_names = [
+        label[0] for label in tracker_constants.DEFAULT_WELL_KNOWN_LABELS]
+    expected_label_names += [
+        'Restrict-View-EditIssue', 'Restrict-AddIssueComment-EditIssue',
+        'Restrict-View-CoreTeam']
+    self.assertEqual(
+        sorted(expected_label_names),
+        sorted(label.label for label in response.label_options))
+
+  def testGetLabelOptions_CustomPermissions(self):
+    self.project.extra_perms = [
+        project_pb2.Project.ExtraPerms(
+            member_id=222,
+            perms=['FooPerm', 'BarPerm'])]
+
+    request = projects_pb2.GetLabelOptionsRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.GetLabelOptions, mc, request)
+
+    expected_label_names = [
+        label[0] for label in tracker_constants.DEFAULT_WELL_KNOWN_LABELS]
+    expected_label_names += [
+        'Restrict-View-EditIssue', 'Restrict-AddIssueComment-EditIssue']
+    expected_label_names += [
+        'Restrict-%s-%s' % (std_perm, custom_perm)
+        for std_perm in permissions.STANDARD_ISSUE_PERMISSIONS
+        for custom_perm in ('BarPerm', 'FooPerm')]
+
+    self.assertEqual(
+        sorted(expected_label_names),
+        sorted(label.label for label in response.label_options))
+
+  def testGetLabelOptions_FieldMasksLabel(self):
+    self.AddField('Type', field_type_str='ENUM_TYPE')
+
+    request = projects_pb2.GetLabelOptionsRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.GetLabelOptions, mc, request)
+
+    expected_label_names = [
+        label[0] for label in tracker_constants.DEFAULT_WELL_KNOWN_LABELS
+        if not label[0].startswith('Type-')
+    ]
+    expected_label_names += [
+        'Restrict-View-EditIssue', 'Restrict-AddIssueComment-EditIssue',
+        'Restrict-View-CoreTeam']
+    self.assertEqual(
+        sorted(expected_label_names),
+        sorted(label.label for label in response.label_options))
+
+  def CallGetStarCount(self):
+    request = projects_pb2.GetProjectStarCountRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    response = self.CallWrapped(
+        self.projects_svcr.GetProjectStarCount, mc, request)
+    return response.star_count
+
+  def CallStar(self, requester='owner@example.com', starred=True):
+    request = projects_pb2.StarProjectRequest(
+        project_name='proj', starred=starred)
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester=requester)
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(
+        self.projects_svcr.StarProject, mc, request)
+    return response.star_count
+
+  def testStarCount_Normal(self):
+    self.assertEqual(0, self.CallGetStarCount())
+    self.assertEqual(1, self.CallStar())
+    self.assertEqual(1, self.CallGetStarCount())
+
+  def testStarCount_StarTwiceSameUser(self):
+    self.assertEqual(1, self.CallStar())
+    self.assertEqual(1, self.CallStar())
+    self.assertEqual(1, self.CallGetStarCount())
+
+  def testStarCount_StarTwiceDifferentUser(self):
+    self.assertEqual(1, self.CallStar())
+    self.assertEqual(2, self.CallStar(requester='user_222@example.com'))
+    self.assertEqual(2, self.CallGetStarCount())
+
+  def testStarCount_RemoveStarTwiceSameUser(self):
+    self.assertEqual(1, self.CallStar())
+    self.assertEqual(1, self.CallGetStarCount())
+
+    self.assertEqual(0, self.CallStar(starred=False))
+    self.assertEqual(0, self.CallStar(starred=False))
+    self.assertEqual(0, self.CallGetStarCount())
+
+  def testStarCount_RemoveStarTwiceDifferentUser(self):
+    self.assertEqual(1, self.CallStar())
+    self.assertEqual(2, self.CallStar(requester='user_222@example.com'))
+    self.assertEqual(2, self.CallGetStarCount())
+
+    self.assertEqual(1, self.CallStar(starred=False))
+    self.assertEqual(
+        0, self.CallStar(requester='user_222@example.com', starred=False))
+    self.assertEqual(0, self.CallGetStarCount())
+
+  def testCheckProjectName_OK(self):
+    """We can check a project name."""
+    request = projects_pb2.CheckProjectNameRequest(project_name='foo')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='admin@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(
+        self.projects_svcr.CheckProjectName, mc, request)
+
+    self.assertEqual('', response.error)
+
+  def testCheckProjectName_InvalidProjectName(self):
+    """We reject an invalid project name."""
+    request = projects_pb2.CheckProjectNameRequest(project_name='Foo')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='admin@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(
+        self.projects_svcr.CheckProjectName, mc, request)
+
+    self.assertNotEqual('', response.error)
+
+  def testCheckProjectName_NotAllowed(self):
+    """Users that can't create a project shouldn't get any information."""
+    request = projects_pb2.CheckProjectNameRequest(project_name='Foo')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='owner@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    with self.assertRaises(permissions.PermissionException):
+      self.CallWrapped(self.projects_svcr.CheckProjectName, mc, request)
+
+  def testCheckProjectName_ProjectAlreadyExists(self):
+    """There is already a project with that name."""
+    request = projects_pb2.CheckProjectNameRequest(project_name='proj')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='admin@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(
+        self.projects_svcr.CheckProjectName, mc, request)
+
+    self.assertNotEqual('', response.error)
+
+  def testCheckComponentName_OK(self):
+    request = projects_pb2.CheckComponentNameRequest(
+        project_name='proj',
+        component_name='Component')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='admin@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(
+        self.projects_svcr.CheckComponentName, mc, request)
+
+    self.assertEqual('', response.error)
+
+  def testCheckComponentName_ParentComponentOK(self):
+    self.services.config.CreateComponentDef(
+        self.cnxn, self.project.project_id, 'Component', 'Docstring',
+        False, [], [], 0, 111, [])
+    request = projects_pb2.CheckComponentNameRequest(
+        project_name='proj',
+        parent_path='Component',
+        component_name='Path')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='admin@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(
+        self.projects_svcr.CheckComponentName, mc, request)
+
+    self.assertEqual('', response.error)
+
+  def testCheckComponentName_InvalidComponentName(self):
+    request = projects_pb2.CheckComponentNameRequest(
+        project_name='proj',
+        component_name='Component-')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='admin@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(
+        self.projects_svcr.CheckComponentName, mc, request)
+
+    self.assertNotEqual('', response.error)
+
+  def testCheckComponentName_ComponentAlreadyExists(self):
+    self.services.config.CreateComponentDef(
+        self.cnxn, self.project.project_id, 'Component', 'Docstring',
+        False, [], [], 0, 111, [])
+    request = projects_pb2.CheckComponentNameRequest(
+        project_name='proj',
+        component_name='Component')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='admin@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(
+        self.projects_svcr.CheckComponentName, mc, request)
+
+    self.assertNotEqual('', response.error)
+
+  def testCheckComponentName_NotAllowedToViewProject(self):
+    self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+    request = projects_pb2.CheckComponentNameRequest(
+        project_name='proj',
+        parent_path='Component',
+        component_name='Path')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='user_444@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    with self.assertRaises(permissions.PermissionException):
+      self.CallWrapped(self.projects_svcr.CheckComponentName, mc, request)
+
+  def testCheckComponentName_ParentComponentDoesntExist(self):
+    request = projects_pb2.CheckComponentNameRequest(
+        project_name='proj',
+        parent_path='Component',
+        component_name='Path')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='admin@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    with self.assertRaises(exceptions.NoSuchComponentException):
+      self.CallWrapped(self.projects_svcr.CheckComponentName, mc, request)
+
+  def testCheckFieldName_OK(self):
+    request = projects_pb2.CheckFieldNameRequest(
+        project_name='proj',
+        field_name='Foo')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='admin@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+    self.assertEqual('', response.error)
+
+  def testCheckFieldName_InvalidFieldName(self):
+    request = projects_pb2.CheckFieldNameRequest(
+        project_name='proj',
+        field_name='**Foo**')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='admin@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+    self.assertNotEqual('', response.error)
+
+  def testCheckFieldName_InvalidFieldName_ApproverSuffix(self):
+    request = projects_pb2.CheckFieldNameRequest(
+        project_name='proj',
+        field_name='Foo-aPprOver')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='admin@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+    self.assertNotEqual('', response.error)
+
+  def testCheckFieldName_FieldAlreadyExists(self):
+    self.AddField('Foo')
+    request = projects_pb2.CheckFieldNameRequest(
+        project_name='proj',
+        field_name='Foo')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='admin@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+    self.assertNotEqual('', response.error)
+
+  def testCheckFieldName_FieldIsPrefixOfAnother(self):
+    self.AddField('Foo-Bar')
+    request = projects_pb2.CheckFieldNameRequest(
+        project_name='proj',
+        field_name='Foo')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='admin@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+    self.assertNotEqual('', response.error)
+
+  def testCheckFieldName_AnotherFieldIsPrefix(self):
+    self.AddField('Foo')
+    request = projects_pb2.CheckFieldNameRequest(
+        project_name='proj',
+        field_name='Foo-Bar')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='admin@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+    self.assertNotEqual('', response.error)
+
+  def testCheckFieldName_NotAllowedToViewProject(self):
+    self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+    request = projects_pb2.CheckFieldNameRequest(
+        project_name='proj',
+        field_name='Foo')
+    mc = monorailcontext.MonorailContext(
+        self.services, cnxn=self.cnxn, requester='user_444@example.com')
+    mc.LookupLoggedInUserPerms(self.project)
+    with self.assertRaises(permissions.PermissionException):
+      self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)