Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/sitewide/test/__init__.py b/sitewide/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/sitewide/test/__init__.py
diff --git a/sitewide/test/custom_404_test.py b/sitewide/test/custom_404_test.py
new file mode 100644
index 0000000..71b52f8
--- /dev/null
+++ b/sitewide/test/custom_404_test.py
@@ -0,0 +1,44 @@
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for the custom_404 servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import httplib
+import unittest
+
+from framework import exceptions
+from services import service_manager
+from sitewide import custom_404
+from testing import fake
+from testing import testing_helpers
+
+
+class Custom404Test(unittest.TestCase):
+
+  def setUp(self):
+    self.services = service_manager.Services(
+        project=fake.ProjectService())
+    self.servlet = custom_404.ErrorPage('req', 'res', services=self.services)
+
+  def testGatherPageData_NoProjectSpecified(self):
+    """Project was not included in URL, so raise exception, will cause 400."""
+    _, mr = testing_helpers.GetRequestObjects(
+        path='/not/a/project/url')
+
+    with self.assertRaises(exceptions.InputException):
+      self.servlet.GatherPageData(mr)
+
+  def testGatherPageData_Normal(self):
+    """Return page_data dict with a 404 response code specified."""
+    _project = self.services.project.TestAddProject('proj')
+    _, mr = testing_helpers.GetRequestObjects(path='/p/proj/junk')
+
+    page_data = self.servlet.GatherPageData(mr)
+    self.assertEqual(
+      {'http_response_code': httplib.NOT_FOUND},
+      page_data)
diff --git a/sitewide/test/group_helpers_test.py b/sitewide/test/group_helpers_test.py
new file mode 100644
index 0000000..af03d08
--- /dev/null
+++ b/sitewide/test/group_helpers_test.py
@@ -0,0 +1,51 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit test for User Group helpers."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from proto import user_pb2
+from proto import usergroup_pb2
+from sitewide import group_helpers
+
+
+class GroupHelpersTest(unittest.TestCase):
+
+  def testGroupVisibilityView(self):
+    gvv_anyone = group_helpers.GroupVisibilityView(
+        usergroup_pb2.MemberVisibility.ANYONE)
+    gvv_members = group_helpers.GroupVisibilityView(
+        usergroup_pb2.MemberVisibility.MEMBERS)
+    gvv_owners = group_helpers.GroupVisibilityView(
+        usergroup_pb2.MemberVisibility.OWNERS)
+    self.assertEqual('Anyone on the Internet', gvv_anyone.name)
+    self.assertEqual('Group Members', gvv_members.name)
+    self.assertEqual('Group Owners', gvv_owners.name)
+
+  def testGroupMemberView(self):
+    user = user_pb2.MakeUser(1, email='test@example.com')
+    gmv = group_helpers.GroupMemberView(user, 888, 'member')
+    self.assertEqual(888, gmv.group_id)
+    self.assertEqual('member', gmv.role)
+
+  def testBuildUserGroupVisibilityOptions(self):
+    vis_views = group_helpers.BuildUserGroupVisibilityOptions()
+    self.assertEqual(3, len(vis_views))
+
+  def testGroupTypeView(self):
+    gt_cia = group_helpers.GroupTypeView(
+        usergroup_pb2.GroupType.CHROME_INFRA_AUTH)
+    gt_mdb = group_helpers.GroupTypeView(
+        usergroup_pb2.GroupType.MDB)
+    self.assertEqual('Chrome-infra-auth', gt_cia.name)
+    self.assertEqual('MDB', gt_mdb.name)
+
+  def testBuildUserGroupTypeOptions(self):
+    group_types = group_helpers.BuildUserGroupTypeOptions()
+    self.assertEqual(4, len(group_types))
diff --git a/sitewide/test/groupadmin_test.py b/sitewide/test/groupadmin_test.py
new file mode 100644
index 0000000..d1f7e0f
--- /dev/null
+++ b/sitewide/test/groupadmin_test.py
@@ -0,0 +1,85 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit test for User Group admin servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from framework import permissions
+from proto import usergroup_pb2
+from services import service_manager
+from sitewide import groupadmin
+from testing import fake
+from testing import testing_helpers
+
+
+class GrouAdminTest(unittest.TestCase):
+  """Tests for the GroupAdmin servlet."""
+
+  def setUp(self):
+    self.services = service_manager.Services(
+        user=fake.UserService(),
+        usergroup=fake.UserGroupService(),
+        project=fake.ProjectService())
+    self.services.user.TestAddUser('a@example.com', 111)
+    self.services.user.TestAddUser('b@example.com', 222)
+    self.services.user.TestAddUser('c@example.com', 333)
+    self.services.user.TestAddUser('group@example.com', 888)
+    self.services.user.TestAddUser('importgroup@example.com', 999)
+    self.services.usergroup.TestAddGroupSettings(888, 'group@example.com')
+    self.services.usergroup.TestAddGroupSettings(
+        999, 'importgroup@example.com', external_group_type='mdb')
+    self.servlet = groupadmin.GroupAdmin(
+        'req', 'res', services=self.services)
+    self.mr = testing_helpers.MakeMonorailRequest()
+    self.mr.viewed_username = 'group@example.com'
+    self.mr.viewed_user_auth.user_id = 888
+
+  def testAssertBasePermission(self):
+    mr = testing_helpers.MakeMonorailRequest(
+        perms=permissions.GetPermissions(None, {}, None))
+    mr.viewed_user_auth.user_id = 888
+    self.assertRaises(
+        permissions.PermissionException,
+        self.servlet.AssertBasePermission, mr)
+    self.services.usergroup.TestAddMembers(888, [111], 'owner')
+    self.servlet.AssertBasePermission(self.mr)
+
+  def testGatherPageData_Normal(self):
+    page_data = self.servlet.GatherPageData(self.mr)
+    self.assertEqual('group@example.com', page_data['groupname'])
+    self.assertEqual('Group Members', page_data['initial_visibility'].name)
+    self.assertEqual(3, len(page_data['visibility_levels']))
+
+  def testGatherPageData_Import(self):
+    mr = testing_helpers.MakeMonorailRequest()
+    mr.viewed_username = 'importgroup@example.com'
+    mr.viewed_user_auth.user_id = 999
+    page_data = self.servlet.GatherPageData(mr)
+    self.assertEqual('importgroup@example.com', page_data['groupname'])
+    self.assertTrue(page_data['import_group'])
+    self.assertEqual('MDB', page_data['initial_group_type'].name)
+
+  def testProcessFormData_Normal(self):
+    post_data = fake.PostData(visibility='0')
+    url = self.servlet.ProcessFormData(self.mr, post_data)
+    self.assertIn('/g/group@example.com/groupadmin', url)
+    group_settings = self.services.usergroup.GetGroupSettings(None, 888)
+    self.assertEqual(usergroup_pb2.MemberVisibility.OWNERS,
+                     group_settings.who_can_view_members)
+
+  def testProcessFormData_Import(self):
+    post_data = fake.PostData(
+        group_type='1', import_group=['on'])
+    url = self.servlet.ProcessFormData(self.mr, post_data)
+    self.assertIn('/g/group@example.com/groupadmin', url)
+    group_settings = self.services.usergroup.GetGroupSettings(None, 888)
+    self.assertEqual(usergroup_pb2.MemberVisibility.OWNERS,
+                     group_settings.who_can_view_members)
+    self.assertEqual(usergroup_pb2.GroupType.MDB,
+                     group_settings.ext_group_type)
diff --git a/sitewide/test/groupcreate_test.py b/sitewide/test/groupcreate_test.py
new file mode 100644
index 0000000..bf7be8d
--- /dev/null
+++ b/sitewide/test/groupcreate_test.py
@@ -0,0 +1,101 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit test for User Group creation servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import settings
+from framework import permissions
+from proto import site_pb2
+from proto import usergroup_pb2
+from services import service_manager
+from sitewide import groupcreate
+from testing import fake
+from testing import testing_helpers
+
+
+class GroupCreateTest(unittest.TestCase):
+  """Tests for the GroupCreate servlet."""
+
+  def setUp(self):
+    self.services = service_manager.Services(
+        user=fake.UserService(),
+        usergroup=fake.UserGroupService(),
+        project=fake.ProjectService())
+    self.servlet = groupcreate.GroupCreate(
+        'req', 'res', services=self.services)
+    self.mr = testing_helpers.MakeMonorailRequest()
+
+  def CheckAssertBasePermissions(
+      self, restriction, expect_admin_ok, expect_nonadmin_ok):
+    old_group_creation_restriction = settings.group_creation_restriction
+    settings.group_creation_restriction = restriction
+
+    # Anon users can never do it
+    mr = testing_helpers.MakeMonorailRequest(
+        perms=permissions.GetPermissions(None, {}, None))
+    self.assertRaises(
+        permissions.PermissionException,
+        self.servlet.AssertBasePermission, mr)
+
+    mr = testing_helpers.MakeMonorailRequest()
+    if expect_admin_ok:
+      self.servlet.AssertBasePermission(mr)
+    else:
+      self.assertRaises(
+          permissions.PermissionException,
+          self.servlet.AssertBasePermission, mr)
+
+    mr = testing_helpers.MakeMonorailRequest(
+        perms=permissions.GetPermissions(mr.auth.user_pb, {111}, None))
+    if expect_nonadmin_ok:
+      self.servlet.AssertBasePermission(mr)
+    else:
+      self.assertRaises(
+          permissions.PermissionException,
+          self.servlet.AssertBasePermission, mr)
+
+    settings.group_creation_restriction = old_group_creation_restriction
+
+  def testAssertBasePermission(self):
+    self.CheckAssertBasePermissions(
+        site_pb2.UserTypeRestriction.ANYONE, True, True)
+    self.CheckAssertBasePermissions(
+        site_pb2.UserTypeRestriction.ADMIN_ONLY, True, False)
+    self.CheckAssertBasePermissions(
+        site_pb2.UserTypeRestriction.NO_ONE, False, False)
+
+  def testGatherPageData(self):
+    page_data = self.servlet.GatherPageData(self.mr)
+    self.assertEqual('', page_data['initial_name'])
+
+  def testProcessFormData_Normal(self):
+    post_data = fake.PostData(
+        groupname=['group@example.com'], visibility='1')
+    url = self.servlet.ProcessFormData(self.mr, post_data)
+    self.assertIn('/g/3444127190/', url)
+    group_id = self.services.user.LookupUserID('cnxn', 'group@example.com')
+    group_settings = self.services.usergroup.GetGroupSettings('cnxn', group_id)
+    self.assertIsNotNone(group_settings)
+    members_after, owners_after = self.services.usergroup.LookupMembers(
+        'cnxn', [group_id])
+    self.assertEqual(0, len(members_after[group_id] + owners_after[group_id]))
+
+  def testProcessFormData_Import(self):
+    post_data = fake.PostData(
+        groupname=['group@example.com'], group_type='1',
+        import_group=['on'])
+    self.servlet.ProcessFormData(self.mr, post_data)
+    group_id = self.services.user.LookupUserID('cnxn', 'group@example.com')
+    group_settings = self.services.usergroup.GetGroupSettings('cnxn', group_id)
+    self.assertIsNotNone(group_settings)
+    self.assertEqual(usergroup_pb2.MemberVisibility.OWNERS,
+                     group_settings.who_can_view_members)
+    self.assertEqual(usergroup_pb2.GroupType.MDB,
+                     group_settings.ext_group_type)
diff --git a/sitewide/test/groupdetail_test.py b/sitewide/test/groupdetail_test.py
new file mode 100644
index 0000000..4440bb8
--- /dev/null
+++ b/sitewide/test/groupdetail_test.py
@@ -0,0 +1,146 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit test for User Group Detail servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from framework import exceptions
+from framework import permissions
+from services import service_manager
+from sitewide import groupdetail
+from testing import fake
+from testing import testing_helpers
+
+
+class GroupDetailTest(unittest.TestCase):
+  """Tests for the GroupDetail servlet."""
+
+  def setUp(self):
+    self.services = service_manager.Services(
+        project=fake.ProjectService(),
+        user=fake.UserService(),
+        usergroup=fake.UserGroupService())
+    self.services.user.TestAddUser('a@example.com', 111)
+    self.services.user.TestAddUser('b@example.com', 222)
+    self.services.user.TestAddUser('c@example.com', 333)
+    self.services.user.TestAddUser('group@example.com', 888)
+    self.services.usergroup.TestAddGroupSettings(888, 'group@example.com')
+    self.servlet = groupdetail.GroupDetail(
+        'req', 'res', services=self.services)
+    self.mr = testing_helpers.MakeMonorailRequest()
+    self.mr.viewed_username = 'group@example.com'
+    self.mr.viewed_user_auth.user_id = 888
+
+  def testAssertBasePermission(self):
+    mr = testing_helpers.MakeMonorailRequest(
+        perms=permissions.GetPermissions(None, {}, None))
+    mr.viewed_user_auth.user_id = 888
+    mr.auth.effective_ids = set([111])
+    self.assertRaises(
+        permissions.PermissionException,
+        self.servlet.AssertBasePermission, mr)
+    self.services.usergroup.TestAddMembers(888, [111], 'member')
+    self.servlet.AssertBasePermission(mr)
+
+  def testAssertBasePermission_IgnoreNoSuchGroup(self):
+    """The permission check does not crash for non-existent user groups."""
+    mr = testing_helpers.MakeMonorailRequest(
+        perms=permissions.GetPermissions(None, {}, None))
+    mr.viewed_user_auth.user_id = 404
+    mr.auth.effective_ids = set([111])
+    self.servlet.AssertBasePermission(mr)
+
+  def testAssertBasePermission_IndirectMembership(self):
+    self.services.usergroup.TestAddGroupSettings(999, 'subgroup@example.com')
+    mr = testing_helpers.MakeMonorailRequest(
+        perms=permissions.GetPermissions(None, {}, None))
+    mr.viewed_user_auth.user_id = 888
+    mr.auth.effective_ids = set([111])
+    self.assertRaises(
+        permissions.PermissionException,
+        self.servlet.AssertBasePermission, mr)
+    self.services.usergroup.TestAddMembers(888, [999], 'member')
+    self.services.usergroup.TestAddMembers(999, [111], 'member')
+    self.servlet.AssertBasePermission(mr)
+
+  def testGatherPagData_ZeroMembers(self):
+    page_data = self.servlet.GatherPageData(self.mr)
+    pagination = page_data['pagination']
+    self.assertEqual(0, len(pagination.visible_results))
+
+  def testGatherPagData_NonzeroMembers(self):
+    self.services.usergroup.TestAddMembers(888, [111, 222, 333])
+    page_data = self.servlet.GatherPageData(self.mr)
+    pagination = page_data['pagination']
+    self.assertEqual(3, len(pagination.visible_results))
+    self.assertEqual(3, pagination.total_count)
+    self.assertEqual(1, pagination.start)
+    self.assertEqual(3, pagination.last)
+    user_view_a, user_view_b, user_view_c = pagination.visible_results
+    self.assertEqual('a@example.com', user_view_a.email)
+    self.assertEqual('b@example.com', user_view_b.email)
+    self.assertEqual('c@example.com', user_view_c.email)
+
+  def testProcessAddMembers_NoneAdded(self):
+    post_data = fake.PostData(addmembers=[''], role=['member'])
+    url = self.servlet.ProcessAddMembers(self.mr, post_data)
+    self.assertIn('/g/group@example.com/?', url)
+    members_after, _ = self.services.usergroup.LookupMembers('cnxn', [888])
+    self.assertEqual(0, len(members_after[888]))
+
+    self.services.usergroup.TestAddMembers(888, [111, 222, 333])
+    url = self.servlet.ProcessAddMembers(self.mr, post_data)
+    self.assertIn('/g/group@example.com/?', url)
+    members_after, _ = self.services.usergroup.LookupMembers('cnxn', [888])
+    self.assertEqual(3, len(members_after[888]))
+
+  def testProcessAddMembers_SomeAdded(self):
+    self.services.usergroup.TestAddMembers(888, [111])
+    post_data = fake.PostData(
+        addmembers=['b@example.com, c@example.com'], role=['member'])
+    url = self.servlet.ProcessAddMembers(self.mr, post_data)
+    self.assertIn('/g/group@example.com/?', url)
+    members_after, _ = self.services.usergroup.LookupMembers('cnxn', [888])
+    self.assertEqual(3, len(members_after[888]))
+
+  def testProcessRemoveMembers_SomeRemoved(self):
+    self.services.usergroup.TestAddMembers(888, [111, 222, 333])
+    post_data = fake.PostData(remove=['b@example.com', 'c@example.com'])
+    url = self.servlet.ProcessRemoveMembers(self.mr, post_data)
+    self.assertIn('/g/group@example.com/?', url)
+    members_after, _ = self.services.usergroup.LookupMembers('cnxn', [888])
+    self.assertEqual(1, len(members_after[888]))
+
+  def testProcessFormData_NoPermission(self):
+    """Group members cannot edit group."""
+    self.services.usergroup.TestAddMembers(888, [111], 'member')
+    mr = testing_helpers.MakeMonorailRequest(
+        perms=permissions.GetPermissions(None, {}, None))
+    mr.viewed_user_auth.user_id = 888
+    mr.auth.effective_ids = set([111])
+    self.assertRaises(permissions.PermissionException,
+                      self.servlet.ProcessFormData, mr, {})
+
+  def testProcessFormData_OwnerPermission(self):
+    """Group owners cannot edit group."""
+    self.services.usergroup.TestAddMembers(888, [111], 'owner')
+    mr = testing_helpers.MakeMonorailRequest(
+        perms=permissions.GetPermissions(None, {}, None))
+    mr.viewed_user_auth.user_id = 888
+    mr.auth.effective_ids = set([111])
+    self.servlet.ProcessFormData(mr, {})
+
+  def testGatherPagData_NoSuchUserGroup(self):
+    """If there is no such user group, raise an exception."""
+    self.mr.viewed_user_auth.user_id = 404
+    self.assertRaises(
+        exceptions.NoSuchGroupException,
+        self.servlet.GatherPageData, self.mr)
+
+
diff --git a/sitewide/test/grouplist_test.py b/sitewide/test/grouplist_test.py
new file mode 100644
index 0000000..9ec6bd5
--- /dev/null
+++ b/sitewide/test/grouplist_test.py
@@ -0,0 +1,84 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit test for User Group List servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from google.appengine.ext import testbed
+
+from framework import permissions
+from services import service_manager
+from sitewide import grouplist
+from testing import fake
+from testing import testing_helpers
+
+
+class GroupListTest(unittest.TestCase):
+  """Tests for the GroupList servlet."""
+
+  def setUp(self):
+    self.services = service_manager.Services(
+        usergroup=fake.UserGroupService())
+    self.servlet = grouplist.GroupList('req', 'res', services=self.services)
+    self.mr = testing_helpers.MakeMonorailRequest()
+    self.testbed = testbed.Testbed()
+    self.testbed.activate()
+    self.testbed.init_memcache_stub()
+    self.testbed.init_datastore_v3_stub()
+
+  def tearDown(self):
+    self.testbed.deactivate()
+
+  def testAssertBasePermission_Anon(self):
+    self.mr.perms = permissions.READ_ONLY_PERMISSIONSET
+    with self.assertRaises(permissions.PermissionException):
+      self.servlet.AssertBasePermission(self.mr)
+
+  def testAssertBasePermission_RegularUsers(self):
+    self.mr.perms = permissions.COMMITTER_ACTIVE_PERMISSIONSET
+    with self.assertRaises(permissions.PermissionException):
+      self.servlet.AssertBasePermission(self.mr)
+
+  def testAssertBasePermission_SiteAdmin(self):
+    self.mr.perms = permissions.ADMIN_PERMISSIONSET
+    self.servlet.AssertBasePermission(self.mr)
+
+  def testGatherPagData_ZeroGroups(self):
+    page_data = self.servlet.GatherPageData(self.mr)
+    self.assertEqual([], page_data['groups'])
+
+  def testGatherPagData_NonzeroGroups(self):
+    self.services.usergroup.TestAddGroupSettings(777, 'group_a@example.com')
+    self.services.usergroup.TestAddGroupSettings(888, 'group_b@example.com')
+    self.services.usergroup.TestAddMembers(888, [111, 222, 333])
+    page_data = self.servlet.GatherPageData(self.mr)
+    group_view_a, group_view_b = page_data['groups']
+    self.assertEqual('group_a@example.com', group_view_a.name)
+    self.assertEqual(0, group_view_a.num_members)
+    self.assertEqual('group_b@example.com', group_view_b.name)
+    self.assertEqual(3, group_view_b.num_members)
+
+  def testProcessFormData_NoPermission(self):
+    mr = testing_helpers.MakeMonorailRequest(
+        perms=permissions.USER_PERMISSIONSET)
+    post_data = fake.PostData(
+      removebtn=[1])
+    self.assertRaises(permissions.PermissionException,
+                      self.servlet.ProcessFormData, mr, post_data)
+
+  def testProcessFormData_Normal(self):
+    self.services.usergroup.TestAddGroupSettings(
+        888, 'group_b@example.com', friend_projects=[789])
+    self.services.usergroup.TestAddMembers(888, [111, 222, 333])
+
+    post_data = fake.PostData(
+        remove=[888],
+        removebtn=[1])
+    self.servlet.ProcessFormData(self.mr, post_data)
+    self.assertNotIn(888, self.services.usergroup.group_settings)
diff --git a/sitewide/test/hostinghome_test.py b/sitewide/test/hostinghome_test.py
new file mode 100644
index 0000000..f51c9ec
--- /dev/null
+++ b/sitewide/test/hostinghome_test.py
@@ -0,0 +1,146 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the Monorail home page."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mock
+import unittest
+
+import ezt
+
+import settings
+from framework import permissions
+from proto import project_pb2
+from proto import site_pb2
+from services import service_manager
+from sitewide import hostinghome
+from sitewide import projectsearch
+from testing import fake
+from testing import testing_helpers
+
+
+class MockProjectSearchPipeline(object):
+
+  def __init__(self, _mr, services):
+    self.visible_results = services.mock_visible_results
+    self.pagination = None
+
+  def SearchForIDs(self, domain=None):
+    pass
+
+  def GetProjectsAndPaginate(self, cnxn, list_page_url):
+    pass
+
+
+class HostingHomeTest(unittest.TestCase):
+
+  def setUp(self):
+    self.services = service_manager.Services(
+        project=fake.ProjectService(),
+        project_star=fake.ProjectStarService())
+    self.services.mock_visible_results = []
+    self.project_a = self.services.project.TestAddProject('a', project_id=1)
+    self.project_b = self.services.project.TestAddProject('b', project_id=2)
+
+    self.servlet = hostinghome.HostingHome('req', 'res', services=self.services)
+    self.mr = testing_helpers.MakeMonorailRequest(user_info={'user_id': 111})
+
+    self.orig_pipeline_class = projectsearch.ProjectSearchPipeline
+    projectsearch.ProjectSearchPipeline = MockProjectSearchPipeline
+
+  def tearDown(self):
+    projectsearch.ProjectSearchPipeline = self.orig_pipeline_class
+
+  def testSearch_ZeroResults(self):
+    self.services.mock_visible_results = []
+    page_data = self.servlet.GatherPageData(self.mr)
+    self.assertEqual([], page_data['projects'])
+
+  def testSearch_NonzeroResults(self):
+    self.services.mock_visible_results = [self.project_a, self.project_b]
+    page_data = self.servlet.GatherPageData(self.mr)
+    self.assertEqual(['a', 'b'],
+                     [pv.project_name for pv in page_data['projects']])
+
+  def testStarCounts(self):
+    """Test the display of star counts on each displayed project."""
+    self.services.mock_visible_results = [self.project_a, self.project_b]
+    # We go straight to the services layer because this is a test set up
+    # rather than an actual user request.
+    self.services.project_star.SetStar('fake cnxn', 1, 111, True)
+    self.services.project_star.SetStar('fake cnxn', 1, 222, True)
+    page_data = self.servlet.GatherPageData(self.mr)
+    project_view_a, project_view_b = page_data['projects']
+    self.assertEqual(2, project_view_a.num_stars)
+    self.assertEqual(0, project_view_b.num_stars)
+
+  def testStarredProjects(self):
+    self.services.mock_visible_results = [self.project_a, self.project_b]
+    self.services.project_star.SetStar('fake cnxn', 1, 111, True)
+    page_data = self.servlet.GatherPageData(self.mr)
+    project_view_a, project_view_b = page_data['projects']
+    self.assertTrue(project_view_a.starred)
+    self.assertFalse(project_view_b.starred)
+
+  def testGatherPageData(self):
+    mr = testing_helpers.MakeMonorailRequest()
+    page_data = self.servlet.GatherPageData(mr)
+    self.assertEqual(settings.learn_more_link, page_data['learn_more_link'])
+
+  def testGatherPageData_CanCreateProject(self):
+    mr = testing_helpers.MakeMonorailRequest()
+    mr.perms = permissions.PermissionSet([permissions.CREATE_PROJECT])
+    page_data = self.servlet.GatherPageData(mr)
+    self.assertEqual(
+      ezt.boolean(settings.project_creation_restriction ==
+                  site_pb2.UserTypeRestriction.ANYONE),
+      page_data['can_create_project'])
+
+    mr.perms = permissions.PermissionSet([])
+    page_data = self.servlet.GatherPageData(mr)
+    self.assertEqual(ezt.boolean(False), page_data['can_create_project'])
+
+  @mock.patch('settings.domain_to_default_project', {})
+  def testMaybeRedirectToDomainDefaultProject_NoMatch(self):
+    """No redirect if the user is not accessing via a configured domain."""
+    mr = testing_helpers.MakeMonorailRequest()
+    mr.request.host = 'example.com'
+    msg = self.servlet._MaybeRedirectToDomainDefaultProject(mr)
+    print('msg: ' + msg)
+    self.assertTrue(msg.startswith('No configured'))
+
+  @mock.patch('settings.domain_to_default_project', {'example.com': 'huh'})
+  def testMaybeRedirectToDomainDefaultProject_NoSuchProject(self):
+    """No redirect if the configured project does not exist."""
+    mr = testing_helpers.MakeMonorailRequest()
+    mr.request.host = 'example.com'
+    print('host is %r' % mr.request.host)
+    msg = self.servlet._MaybeRedirectToDomainDefaultProject(mr)
+    print('msg: ' + msg)
+    self.assertTrue(msg.endswith('not found'))
+
+  @mock.patch('settings.domain_to_default_project', {'example.com': 'a'})
+  def testMaybeRedirectToDomainDefaultProject_CantView(self):
+    """No redirect if the user can't view the configured project."""
+    self.project_a.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+    mr = testing_helpers.MakeMonorailRequest()
+    mr.request.host = 'example.com'
+    msg = self.servlet._MaybeRedirectToDomainDefaultProject(mr)
+    print('msg: ' + msg)
+    self.assertTrue(msg.startswith('User cannot'))
+
+  @mock.patch('settings.domain_to_default_project', {'example.com': 'a'})
+  def testMaybeRedirectToDomainDefaultProject_Redirect(self):
+    """We redirect if there's a configured project that the user can view."""
+    mr = testing_helpers.MakeMonorailRequest()
+    mr.request.host = 'example.com'
+    self.servlet.redirect = mock.Mock()
+    msg = self.servlet._MaybeRedirectToDomainDefaultProject(mr)
+    print('msg: ' + msg)
+    self.assertTrue(msg.startswith('Redirected'))
+    self.servlet.redirect.assert_called_once()
diff --git a/sitewide/test/moved_test.py b/sitewide/test/moved_test.py
new file mode 100644
index 0000000..04b9165
--- /dev/null
+++ b/sitewide/test/moved_test.py
@@ -0,0 +1,113 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for the moved project notification page servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import webapp2
+
+from framework import exceptions
+from services import service_manager
+from sitewide import moved
+from testing import fake
+from testing import testing_helpers
+
+
+class MovedTest(unittest.TestCase):
+
+  def setUp(self):
+    self.services = service_manager.Services(
+        project=fake.ProjectService())
+    self.servlet = moved.ProjectMoved('req', 'res', services=self.services)
+    self.old_project = 'old-project'
+
+  def testGatherPageData_NoProjectSpecified(self):
+    # Project was not included in URL, so raise exception, will cause 400.
+    _, mr = testing_helpers.GetRequestObjects(
+        path='/hosting/moved')
+
+    with self.assertRaises(exceptions.InputException):
+      self.servlet.GatherPageData(mr)
+
+  def testGatherPageData_NoSuchProject(self):
+    # Project doesn't exist, so 404 NOT FOUND.
+    _, mr = testing_helpers.GetRequestObjects(
+        path='/hosting/moved?project=nonexistent')
+
+    with self.assertRaises(webapp2.HTTPException) as cm:
+      self.servlet.GatherPageData(mr)
+    self.assertEqual(404, cm.exception.code)
+
+  def testGatherPageData_NotMoved(self):
+    # Project exists but has not been moved, so 400 BAD_REQUEST.
+    self.services.project.TestAddProject(self.old_project)
+    _, mr = testing_helpers.GetRequestObjects(
+        path='/hosting/moved?project=%s' % self.old_project)
+
+    with self.assertRaises(webapp2.HTTPException) as cm:
+      self.servlet.GatherPageData(mr)
+    self.assertEqual(400, cm.exception.code)
+
+  def testGatherPageData_URL(self):
+    # Display the moved_to url if it is valid.
+    project = self.services.project.TestAddProject(self.old_project)
+    project.moved_to = 'https://other-tracker.bugs'
+    _, mr = testing_helpers.GetRequestObjects(
+        path='/hosting/moved?project=%s' % self.old_project)
+
+    page_data = self.servlet.GatherPageData(mr)
+    self.assertItemsEqual(
+        ['project_name', 'moved_to_url'],
+        list(page_data.keys()))
+    self.assertEqual(self.old_project, page_data['project_name'])
+    self.assertEqual('https://other-tracker.bugs', page_data['moved_to_url'])
+
+  def testGatherPageData_ProjectName(self):
+    # Construct the moved-to url from just the project name.
+    project = self.services.project.TestAddProject(self.old_project)
+    project.moved_to = 'new-project'
+    _, mr = testing_helpers.GetRequestObjects(
+        path='/hosting/moved?project=%s' % self.old_project)
+
+    page_data = self.servlet.GatherPageData(mr)
+    self.assertItemsEqual(
+        ['project_name', 'moved_to_url'],
+        list(page_data.keys()))
+    self.assertEqual(self.old_project, page_data['project_name'])
+    self.assertEqual('http://127.0.0.1/p/new-project/',
+                     page_data['moved_to_url'])
+
+  def testGatherPageData_HttpProjectName(self):
+    # A project named "http-foo" gets treated as a project, not a url.
+    project = self.services.project.TestAddProject(self.old_project)
+    project.moved_to = 'http-project'
+    _, mr = testing_helpers.GetRequestObjects(
+        path='/hosting/moved?project=%s' % self.old_project)
+
+    page_data = self.servlet.GatherPageData(mr)
+    self.assertItemsEqual(
+        ['project_name', 'moved_to_url'],
+        list(page_data.keys()))
+    self.assertEqual(self.old_project, page_data['project_name'])
+    self.assertEqual('http://127.0.0.1/p/http-project/',
+                     page_data['moved_to_url'])
+
+  def testGatherPageData_BadScheme(self):
+    # We only display URLs that start with 'http(s)://'.
+    project = self.services.project.TestAddProject(self.old_project)
+    project.moved_to = 'javascript:alert(1)'
+    _, mr = testing_helpers.GetRequestObjects(
+        path='/hosting/moved?project=%s' % self.old_project)
+
+    page_data = self.servlet.GatherPageData(mr)
+    self.assertItemsEqual(
+        ['project_name', 'moved_to_url'],
+        list(page_data.keys()))
+    self.assertEqual(self.old_project, page_data['project_name'])
+    self.assertEqual('#invalid-destination-url', page_data['moved_to_url'])
diff --git a/sitewide/test/projectcreate_test.py b/sitewide/test/projectcreate_test.py
new file mode 100644
index 0000000..8f468dd
--- /dev/null
+++ b/sitewide/test/projectcreate_test.py
@@ -0,0 +1,74 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittests for the Project Creation servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import settings
+from framework import permissions
+from proto import project_pb2
+from proto import site_pb2
+from services import service_manager
+from sitewide import projectcreate
+from testing import fake
+from testing import testing_helpers
+
+
+class ProjectCreateTest(unittest.TestCase):
+
+  def setUp(self):
+    services = service_manager.Services()
+    self.servlet = projectcreate.ProjectCreate('req', 'res', services=services)
+
+  def CheckAssertBasePermissions(
+      self, restriction, expect_admin_ok, expect_nonadmin_ok):
+    old_project_creation_restriction = settings.project_creation_restriction
+    settings.project_creation_restriction = restriction
+
+    # Anon users can never do it
+    mr = testing_helpers.MakeMonorailRequest(
+        perms=permissions.GetPermissions(None, {}, None))
+    self.assertRaises(
+        permissions.PermissionException,
+        self.servlet.AssertBasePermission, mr)
+
+    mr = testing_helpers.MakeMonorailRequest()
+    if expect_admin_ok:
+      self.servlet.AssertBasePermission(mr)
+    else:
+      self.assertRaises(
+          permissions.PermissionException,
+          self.servlet.AssertBasePermission, mr)
+
+    mr = testing_helpers.MakeMonorailRequest(
+        perms=permissions.GetPermissions(mr.auth.user_pb, {111}, None))
+    if expect_nonadmin_ok:
+      self.servlet.AssertBasePermission(mr)
+    else:
+      self.assertRaises(
+          permissions.PermissionException,
+          self.servlet.AssertBasePermission, mr)
+
+    settings.project_creation_restriction = old_project_creation_restriction
+
+  def testAssertBasePermission(self):
+    self.CheckAssertBasePermissions(
+        site_pb2.UserTypeRestriction.ANYONE, True, True)
+    self.CheckAssertBasePermissions(
+        site_pb2.UserTypeRestriction.ADMIN_ONLY, True, False)
+    self.CheckAssertBasePermissions(
+        site_pb2.UserTypeRestriction.NO_ONE, False, False)
+
+  def testGatherPageData(self):
+    mr = testing_helpers.MakeMonorailRequest()
+    page_data = self.servlet.GatherPageData(mr)
+    self.assertEqual('', page_data['initial_name'])
+    self.assertEqual('', page_data['initial_summary'])
+    self.assertEqual('', page_data['initial_description'])
+    self.assertEqual([], page_data['labels'])
diff --git a/sitewide/test/projectsearch_test.py b/sitewide/test/projectsearch_test.py
new file mode 100644
index 0000000..a0d941d
--- /dev/null
+++ b/sitewide/test/projectsearch_test.py
@@ -0,0 +1,75 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittests for the projectsearch module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mock
+import unittest
+
+from framework import profiler
+from proto import project_pb2
+from services import service_manager
+from sitewide import projectsearch
+from testing import fake
+from testing import testing_helpers
+
+
+class ProjectSearchTest(unittest.TestCase):
+
+  def setUp(self):
+    self.services = service_manager.Services(
+        project=fake.ProjectService())
+    self.services.project.GetVisibleLiveProjects = mock.MagicMock()
+
+    for idx, letter in enumerate('abcdefghijklmnopqrstuvwxyz'):
+      self.services.project.TestAddProject(letter, project_id=idx + 1)
+    for idx in range(27, 110):
+      self.services.project.TestAddProject(str(idx), project_id=idx)
+
+    self.addCleanup(mock.patch.stopall())
+
+  def TestPipeline(self, expected_last, expected_len):
+    mr = testing_helpers.MakeMonorailRequest()
+    mr.can = 1
+
+    pipeline = projectsearch.ProjectSearchPipeline(mr, self.services)
+    pipeline.SearchForIDs()
+    pipeline.GetProjectsAndPaginate('fake cnxn', '/hosting/search')
+    self.assertEqual(1, pipeline.pagination.start)
+    self.assertEqual(expected_last, pipeline.pagination.last)
+    self.assertEqual(expected_len, len(pipeline.visible_results))
+
+    return pipeline
+
+  def testZeroResults(self):
+    self.services.project.GetVisibleLiveProjects.return_value = []
+
+    pipeline = self.TestPipeline(0, 0)
+
+    self.services.project.GetVisibleLiveProjects.assert_called_once()
+    self.assertListEqual([], pipeline.visible_results)
+
+  def testNonzeroResults(self):
+    self.services.project.GetVisibleLiveProjects.return_value = [1, 2, 3]
+
+    pipeline = self.TestPipeline(3, 3)
+
+    self.services.project.GetVisibleLiveProjects.assert_called_once()
+    self.assertListEqual(
+        [1, 2, 3], [p.project_id for p in pipeline.visible_results])
+
+  def testTwoPageResults(self):
+    """Test more than one pagination page of results."""
+    self.services.project.GetVisibleLiveProjects.return_value = list(
+        range(1, 106))
+
+    pipeline = self.TestPipeline(100, 100)
+
+    self.services.project.GetVisibleLiveProjects.assert_called_once()
+    self.assertEqual(
+        '/hosting/search?num=100&start=100', pipeline.pagination.next_url)
diff --git a/sitewide/test/sitewide_helpers_test.py b/sitewide/test/sitewide_helpers_test.py
new file mode 100644
index 0000000..d292b6f
--- /dev/null
+++ b/sitewide/test/sitewide_helpers_test.py
@@ -0,0 +1,170 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for the sitewide_helpers module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from proto import project_pb2
+from services import service_manager
+from sitewide import sitewide_helpers
+from testing import fake
+
+
+REGULAR_USER_ID = 111
+ADMIN_USER_ID = 222
+OTHER_USER_ID = 333
+
+# Test project IDs
+REGULAR_OWNER_LIVE = 1001
+REGULAR_OWNER_ARCHIVED = 1002
+REGULAR_OWNER_DELETABLE = 1003
+REGULAR_COMMITTER_LIVE = 2001
+REGULAR_COMMITTER_ARCHIVED = 2002
+REGULAR_COMMITTER_DELETABLE = 2003
+OTHER_OWNER_LIVE = 3001
+OTHER_OWNER_ARCHIVED = 3002
+OTHER_OWNER_DELETABLE = 3003
+OTHER_COMMITTER_LIVE = 4001
+MEMBERS_ONLY = 5001
+
+
+class HelperFunctionsTest(unittest.TestCase):
+
+  def setUp(self):
+    self.services = service_manager.Services(
+        project=fake.ProjectService(),
+        user=fake.UserService(),
+        project_star=fake.ProjectStarService())
+    self.cnxn = 'fake cnxn'
+
+    for user_id in (ADMIN_USER_ID, REGULAR_USER_ID, OTHER_USER_ID):
+      self.services.user.TestAddUser('ignored_%s@gmail.com' % user_id, user_id)
+
+    self.regular_owner_live = self.services.project.TestAddProject(
+        'regular-owner-live', state=project_pb2.ProjectState.LIVE,
+        owner_ids=[REGULAR_USER_ID], project_id=REGULAR_OWNER_LIVE)
+    self.regular_owner_archived = self.services.project.TestAddProject(
+        'regular-owner-archived', state=project_pb2.ProjectState.ARCHIVED,
+        owner_ids=[REGULAR_USER_ID], project_id=REGULAR_OWNER_ARCHIVED)
+    self.regular_owner_deletable = self.services.project.TestAddProject(
+        'regular-owner-deletable', state=project_pb2.ProjectState.DELETABLE,
+        owner_ids=[REGULAR_USER_ID], project_id=REGULAR_OWNER_DELETABLE)
+    self.regular_committer_live = self.services.project.TestAddProject(
+        'regular-committer-live', state=project_pb2.ProjectState.LIVE,
+        committer_ids=[REGULAR_USER_ID], project_id=REGULAR_COMMITTER_LIVE)
+    self.regular_committer_archived = self.services.project.TestAddProject(
+        'regular-committer-archived', state=project_pb2.ProjectState.ARCHIVED,
+        committer_ids=[REGULAR_USER_ID], project_id=REGULAR_COMMITTER_ARCHIVED)
+    self.regular_committer_deletable = self.services.project.TestAddProject(
+        'regular-committer-deletable', state=project_pb2.ProjectState.DELETABLE,
+        committer_ids=[REGULAR_USER_ID], project_id=REGULAR_COMMITTER_DELETABLE)
+    self.other_owner_live = self.services.project.TestAddProject(
+        'other-owner-live', state=project_pb2.ProjectState.LIVE,
+        owner_ids=[OTHER_USER_ID], project_id=OTHER_OWNER_LIVE)
+    self.other_owner_archived = self.services.project.TestAddProject(
+        'other-owner-archived', state=project_pb2.ProjectState.ARCHIVED,
+        owner_ids=[OTHER_USER_ID], project_id=OTHER_OWNER_ARCHIVED)
+    self.other_owner_deletable = self.services.project.TestAddProject(
+        'other-owner-deletable', state=project_pb2.ProjectState.DELETABLE,
+        owner_ids=[OTHER_USER_ID], project_id=OTHER_OWNER_DELETABLE)
+    self.other_committer_live = self.services.project.TestAddProject(
+        'other-committer-live', state=project_pb2.ProjectState.LIVE,
+        committer_ids=[OTHER_USER_ID], project_id=OTHER_COMMITTER_LIVE)
+
+    self.regular_user = self.services.user.GetUser(self.cnxn, REGULAR_USER_ID)
+
+    self.admin_user = self.services.user.TestAddUser(
+        'administrator@chromium.org', ADMIN_USER_ID)
+    self.admin_user.is_site_admin = True
+
+    self.other_user = self.services.user.GetUser(self.cnxn, OTHER_USER_ID)
+
+    self.members_only_project = self.services.project.TestAddProject(
+        'members-only', owner_ids=[REGULAR_USER_ID], project_id=MEMBERS_ONLY)
+    self.members_only_project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+
+  def assertProjectsAnyOrder(self, actual_projects, *expected_projects):
+    # Check names rather than Project objects so that output is easier to read.
+    actual_names = [p.project_name for p in actual_projects]
+    expected_names = [p.project_name for p in expected_projects]
+    self.assertItemsEqual(expected_names, actual_names)
+
+  def testFilterViewableProjects_CantViewArchived(self):
+    projects = list(sitewide_helpers.FilterViewableProjects(
+        list(self.services.project.test_projects.values()),
+        self.regular_user, {REGULAR_USER_ID}))
+    self.assertProjectsAnyOrder(
+        projects, self.regular_owner_live, self.regular_committer_live,
+        self.other_owner_live, self.other_committer_live,
+        self.members_only_project)
+
+  def testFilterViewableProjects_NonMemberCantViewMembersOnly(self):
+    projects = list(sitewide_helpers.FilterViewableProjects(
+        list(self.services.project.test_projects.values()),
+        self.other_user, {OTHER_USER_ID}))
+    self.assertProjectsAnyOrder(
+        projects, self.regular_owner_live, self.regular_committer_live,
+        self.other_owner_live, self.other_committer_live)
+
+  def testFilterViewableProjects_AdminCanViewAny(self):
+    projects = list(sitewide_helpers.FilterViewableProjects(
+        list(self.services.project.test_projects.values()),
+        self.admin_user, {ADMIN_USER_ID}))
+    self.assertProjectsAnyOrder(
+        projects, self.regular_owner_live, self.regular_committer_live,
+        self.other_owner_live, self.other_committer_live,
+        self.members_only_project)
+
+  def testGetStarredProjects_OnlyViewableLiveStarred(self):
+    viewed_user_id = 123
+    for p in self.services.project.test_projects.values():
+      # We go straight to the services layer because this is a test set up
+      # rather than an actual user request.
+      self.services.project_star.SetStar(
+          self.cnxn, p.project_id, viewed_user_id, True)
+
+    self.assertProjectsAnyOrder(
+        sitewide_helpers.GetViewableStarredProjects(
+            self.cnxn, self.services, viewed_user_id,
+            {REGULAR_USER_ID}, self.regular_user),
+        self.regular_owner_live, self.regular_committer_live,
+        self.other_owner_live, self.other_committer_live,
+        self.members_only_project)
+
+  def testGetStarredProjects_MembersOnly(self):
+    # Both users were able to star the project in the past.  The stars do not
+    # go away even if access to the project changes.
+    self.services.project_star.SetStar(
+        self.cnxn, self.members_only_project.project_id, REGULAR_USER_ID, True)
+    self.services.project_star.SetStar(
+        self.cnxn, self.members_only_project.project_id, OTHER_USER_ID, True)
+
+    # But now, only one of them is currently a member, so only regular_user
+    # can see the starred project in the lists.
+    self.assertProjectsAnyOrder(
+        sitewide_helpers.GetViewableStarredProjects(
+            self.cnxn, self.services, REGULAR_USER_ID, {REGULAR_USER_ID},
+            self.regular_user),
+        self.members_only_project)
+    self.assertProjectsAnyOrder(
+        sitewide_helpers.GetViewableStarredProjects(
+            self.cnxn, self.services, OTHER_USER_ID, {REGULAR_USER_ID},
+            self.regular_user),
+        self.members_only_project)
+
+    # The other user cannot see the project, so they do not see it in either
+    # list of starred projects.
+    self.assertProjectsAnyOrder(
+        sitewide_helpers.GetViewableStarredProjects(
+            self.cnxn, self.services, REGULAR_USER_ID, {OTHER_USER_ID},
+            self.other_user))  # No expected projects listed.
+    self.assertProjectsAnyOrder(
+        sitewide_helpers.GetViewableStarredProjects(
+            self.cnxn, self.services, OTHER_USER_ID, {OTHER_USER_ID},
+            self.other_user))  # No expected projects listed.
diff --git a/sitewide/test/sitewide_views_test.py b/sitewide/test/sitewide_views_test.py
new file mode 100644
index 0000000..ed2515f
--- /dev/null
+++ b/sitewide/test/sitewide_views_test.py
@@ -0,0 +1,26 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for sitewide_views module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from proto import usergroup_pb2
+from sitewide import sitewide_views
+
+
+class GroupViewTest(unittest.TestCase):
+
+  def testConstructor(self):
+    group_settings = usergroup_pb2.MakeSettings('anyone')
+    view = sitewide_views.GroupView('groupname', 123, group_settings, 999)
+
+    self.assertEqual('groupname', view.name)
+    self.assertEqual(123, view.num_members)
+    self.assertEqual('ANYONE', view.who_can_view_members)
+    self.assertEqual('/g/999/', view.detail_url)
diff --git a/sitewide/test/userprofile_test.py b/sitewide/test/userprofile_test.py
new file mode 100644
index 0000000..b830fb7
--- /dev/null
+++ b/sitewide/test/userprofile_test.py
@@ -0,0 +1,252 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the user profile page."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mock
+import unittest
+import logging
+import webapp2
+import ezt
+
+from framework import framework_helpers
+from framework import framework_views
+from framework import permissions
+from proto import project_pb2
+from proto import user_pb2
+from services import service_manager
+from sitewide import userprofile
+from testing import fake
+from testing import testing_helpers
+
+from google.appengine.ext import testbed
+
+REGULAR_USER_ID = 111
+ADMIN_USER_ID = 222
+OTHER_USER_ID = 333
+STATES = {
+    'live': project_pb2.ProjectState.LIVE,
+    'archived': project_pb2.ProjectState.ARCHIVED,
+}
+
+
+def MakeReqInfo(
+    user_pb, user_id, viewed_user_pb, viewed_user_id, viewed_user_name,
+    perms=permissions.USER_PERMISSIONSET):
+  mr = fake.MonorailRequest(None, perms=perms)
+  mr.auth.user_pb = user_pb
+  mr.auth.user_id = user_id
+  mr.auth.effective_ids = {user_id}
+  mr.viewed_user_auth.email = viewed_user_name
+  mr.viewed_user_auth.user_pb = viewed_user_pb
+  mr.viewed_user_auth.user_id = viewed_user_id
+  mr.viewed_user_auth.effective_ids = {viewed_user_id}
+  mr.viewed_user_auth.user_view = framework_views.UserView(viewed_user_pb)
+  mr.viewed_user_name = viewed_user_name
+  mr.request = webapp2.Request.blank("/")
+  return mr
+
+
+class UserProfileTest(unittest.TestCase):
+
+  def setUp(self):
+    self.patcher_1 = mock.patch(
+      'framework.framework_helpers.UserSettings.GatherUnifiedSettingsPageData')
+    self.mock_guspd = self.patcher_1.start()
+    self.mock_guspd.return_value = {'unified': None}
+
+    services = service_manager.Services(
+        project=fake.ProjectService(),
+        user=fake.UserService(),
+        usergroup=fake.UserGroupService(),
+        project_star=fake.ProjectStarService(),
+        user_star=fake.UserStarService())
+    self.servlet = userprofile.UserProfile('req', 'res', services=services)
+
+    for user_id in (
+        REGULAR_USER_ID, ADMIN_USER_ID, OTHER_USER_ID):
+      services.user.TestAddUser('%s@gmail.com' % user_id, user_id)
+
+    for user in ['regular', 'other']:
+      for relation in ['owner', 'member']:
+        for state_name, state in STATES.items():
+          services.project.TestAddProject(
+              '%s-%s-%s' % (user, relation, state_name), state=state)
+
+    # Add projects
+    for state_name, state in STATES.items():
+      services.project.TestAddProject(
+          'regular-owner-%s' % state_name, state=state,
+          owner_ids=[REGULAR_USER_ID])
+      services.project.TestAddProject(
+          'regular-member-%s' % state_name, state=state,
+          committer_ids=[REGULAR_USER_ID])
+      services.project.TestAddProject(
+          'other-owner-%s' % state_name, state=state,
+          owner_ids=[OTHER_USER_ID])
+      services.project.TestAddProject(
+          'other-member-%s' % state_name, state=state,
+          committer_ids=[OTHER_USER_ID])
+
+    self.regular_user = services.user.GetUser('fake cnxn', REGULAR_USER_ID)
+    self.admin_user = services.user.GetUser('fake cnxn', ADMIN_USER_ID)
+    self.admin_user.is_site_admin = True
+    self.other_user = services.user.GetUser('fake cnxn', OTHER_USER_ID)
+
+    self.testbed = testbed.Testbed()
+    self.testbed.activate()
+    self.testbed.init_memcache_stub()
+    self.testbed.init_datastore_v3_stub()
+
+  def tearDown(self):
+    self.testbed.deactivate()
+    mock.patch.stopall()
+
+  def assertProjectsAnyOrder(self, value_to_test, *expected_project_names):
+    actual_project_names = [project_view.project_name
+                            for project_view in value_to_test]
+    self.assertItemsEqual(expected_project_names, actual_project_names)
+
+  def testGatherPageData_RegularUserViewingOtherUserProjects(self):
+    """A user can see the other users' live projects, but not archived ones."""
+    mr = MakeReqInfo(
+        self.regular_user, REGULAR_USER_ID, self.other_user,
+        OTHER_USER_ID, 'other@xyz.com')
+
+    page_data = self.servlet.GatherPageData(mr)
+
+    self.assertProjectsAnyOrder(page_data['owner_of_projects'],
+                                'other-owner-live')
+    self.assertProjectsAnyOrder(page_data['committer_of_projects'],
+                                'other-member-live')
+    self.assertFalse(page_data['owner_of_archived_projects'])
+    self.assertEqual('ot...@xyz.com', page_data['viewed_user_display_name'])
+    self.assertEqual(ezt.boolean(False), page_data['can_delete_user'])
+    self.mock_guspd.assert_called_once_with(
+        111, mr.viewed_user_auth.user_view, mr.viewed_user_auth.user_pb,
+        None)
+
+  def testGatherPageData_RegularUserViewingOwnProjects(self):
+    """A user can see all their own projects: live or archived."""
+    mr = MakeReqInfo(
+        self.regular_user, REGULAR_USER_ID, self.regular_user,
+        REGULAR_USER_ID, 'self@xyz.com')
+
+    page_data = self.servlet.GatherPageData(mr)
+
+    self.assertEqual('self@xyz.com', page_data['viewed_user_display_name'])
+    self.assertEqual(ezt.boolean(False), page_data['can_delete_user'])
+    self.assertProjectsAnyOrder(page_data['owner_of_projects'],
+                                'regular-owner-live')
+    self.assertProjectsAnyOrder(page_data['committer_of_projects'],
+                                'regular-member-live')
+    self.assertProjectsAnyOrder(
+        page_data['owner_of_archived_projects'],
+        'regular-owner-archived')
+    self.mock_guspd.assert_called_once_with(
+        111, mr.viewed_user_auth.user_view, mr.viewed_user_auth.user_pb,
+        None)
+
+  def testGatherPageData_RegularUserViewingStarredUsers(self):
+    """A user can see display names of other users that they starred."""
+    mr = MakeReqInfo(
+        self.regular_user, REGULAR_USER_ID, self.regular_user,
+        REGULAR_USER_ID, 'self@xyz.com')
+    self.servlet.services.user_star.SetStar(
+        'cnxn', OTHER_USER_ID, REGULAR_USER_ID, True)
+
+    page_data = self.servlet.GatherPageData(mr)
+
+    starred_users = page_data['starred_users']
+    self.assertEqual(1, len(starred_users))
+    self.assertEqual('333@gmail.com', starred_users[0].email)
+    self.assertEqual('["3...@gmail.com"]', page_data['starred_users_json'])
+    self.mock_guspd.assert_called_once_with(
+        111, mr.viewed_user_auth.user_view, mr.viewed_user_auth.user_pb,
+        None)
+
+  def testGatherPageData_AdminViewingOtherUserAddress(self):
+    """Site admins always see full email addresses of other users."""
+    mr = MakeReqInfo(
+        self.admin_user, ADMIN_USER_ID, self.other_user,
+        OTHER_USER_ID, 'other@xyz.com',
+        perms=permissions.ADMIN_PERMISSIONSET)
+
+    page_data = self.servlet.GatherPageData(mr)
+
+    self.assertEqual('other@xyz.com', page_data['viewed_user_display_name'])
+    self.assertEqual(ezt.boolean(True), page_data['can_delete_user'])
+    self.mock_guspd.assert_called_once_with(
+        222, mr.viewed_user_auth.user_view, mr.viewed_user_auth.user_pb,
+        mock.ANY)
+
+  def testGatherPageData_RegularUserViewingOtherUserAddressUnobscured(self):
+    """Email should be revealed to others depending on obscure_email."""
+    mr = MakeReqInfo(
+        self.regular_user, REGULAR_USER_ID, self.other_user,
+        OTHER_USER_ID, 'other@xyz.com')
+    mr.viewed_user_auth.user_view.obscure_email = False
+
+    page_data = self.servlet.GatherPageData(mr)
+
+    self.assertEqual('other@xyz.com', page_data['viewed_user_display_name'])
+    self.mock_guspd.assert_called_once_with(
+        111, mr.viewed_user_auth.user_view, mr.viewed_user_auth.user_pb,
+        None)
+
+  def testGatherPageData_RegularUserViewingOtherUserAddressObscured(self):
+    """Email should be revealed to others depending on obscure_email."""
+    mr = MakeReqInfo(
+        self.regular_user, REGULAR_USER_ID, self.other_user,
+        OTHER_USER_ID, 'other@xyz.com')
+    mr.viewed_user_auth.user_view.obscure_email = True
+
+    page_data = self.servlet.GatherPageData(mr)
+
+    self.assertEqual('ot...@xyz.com', page_data['viewed_user_display_name'])
+    self.assertEqual(ezt.boolean(False), page_data['can_delete_user'])
+    self.mock_guspd.assert_called_once_with(
+        111, mr.viewed_user_auth.user_view, mr.viewed_user_auth.user_pb,
+        None)
+
+  def testGatherPageData_NoLinkedAccounts(self):
+    """An account with no linked accounts should not show anything linked."""
+    mr = MakeReqInfo(
+        self.regular_user, REGULAR_USER_ID, self.other_user,
+        OTHER_USER_ID, 'other@xyz.com')
+
+    page_data = self.servlet.GatherPageData(mr)
+
+    self.assertIsNone(page_data['linked_parent'])
+    self.assertEqual([], page_data['linked_children'])
+
+  def testGatherPageData_ParentAccounts(self):
+    """An account with a parent linked account should show it."""
+    self.other_user.linked_parent_id = REGULAR_USER_ID
+    mr = MakeReqInfo(
+        self.regular_user, REGULAR_USER_ID, self.other_user,
+        OTHER_USER_ID, 'other@xyz.com')
+
+    page_data = self.servlet.GatherPageData(mr)
+
+    self.assertEqual('111@gmail.com', page_data['linked_parent'].email)
+    self.assertEqual([], page_data['linked_children'])
+
+  def testGatherPageData_ChildAccounts(self):
+    """An account with a child linked account should show them."""
+    self.other_user.linked_child_ids = [REGULAR_USER_ID, ADMIN_USER_ID]
+    mr = MakeReqInfo(
+        self.regular_user, REGULAR_USER_ID, self.other_user,
+        OTHER_USER_ID, 'other@xyz.com')
+
+    page_data = self.servlet.GatherPageData(mr)
+
+    self.assertEqual(None, page_data['linked_parent'])
+    self.assertEqual(
+        ['111@gmail.com', '222@gmail.com'],
+        [uv.email for uv in page_data['linked_children']])
diff --git a/sitewide/test/usersettings_test.py b/sitewide/test/usersettings_test.py
new file mode 100644
index 0000000..54c14ae
--- /dev/null
+++ b/sitewide/test/usersettings_test.py
@@ -0,0 +1,66 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the user settings page."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import mox
+
+from framework import framework_helpers
+from framework import permissions
+from framework import template_helpers
+from proto import user_pb2
+from services import service_manager
+from sitewide import usersettings
+from testing import fake
+from testing import testing_helpers
+
+
+class UserSettingsTest(unittest.TestCase):
+
+  def setUp(self):
+    self.mox = mox.Mox()
+    self.services = service_manager.Services(user=fake.UserService())
+    self.servlet = usersettings.UserSettings(
+        'req', 'res', services=self.services)
+
+  def tearDown(self):
+    self.mox.UnsetStubs()
+
+  def testAssertBasePermission(self):
+    mr = testing_helpers.MakeMonorailRequest()
+    mr.auth.user_id = 111
+
+    # The following should return without exception.
+    self.servlet.AssertBasePermission(mr)
+
+    # No logged in user means anonymous access, should raise error.
+    mr.auth.user_id = 0
+    self.assertRaises(permissions.PermissionException,
+                      self.servlet.AssertBasePermission, mr)
+
+  def testGatherPageData(self):
+    self.mox.StubOutWithMock(
+        framework_helpers.UserSettings, 'GatherUnifiedSettingsPageData')
+    framework_helpers.UserSettings.GatherUnifiedSettingsPageData(
+        0, None, mox.IsA(user_pb2.User), mox.IsA(user_pb2.UserPrefs)
+        ).AndReturn({'unified': None})
+    self.mox.ReplayAll()
+
+    mr = testing_helpers.MakeMonorailRequest()
+    page_data = self.servlet.GatherPageData(mr)
+
+    self.assertItemsEqual(
+        ['logged_in_user_pb', 'unified', 'user_tab_mode',
+         'viewed_user', 'offer_saved_queries_subtab', 'viewing_self'],
+        list(page_data.keys()))
+    self.assertEqual(template_helpers.PBProxy(mr.auth.user_pb),
+                     page_data['logged_in_user_pb'])
+
+    self.mox.VerifyAll()
diff --git a/sitewide/test/userupdates_test.py b/sitewide/test/userupdates_test.py
new file mode 100644
index 0000000..efae9bc
--- /dev/null
+++ b/sitewide/test/userupdates_test.py
@@ -0,0 +1,115 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittests for monorail.sitewide.userupdates."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import mox
+
+from features import activities
+from services import service_manager
+from sitewide import sitewide_helpers
+from sitewide import userupdates
+from testing import fake
+from testing import testing_helpers
+
+
+class ProjectUpdatesTest(unittest.TestCase):
+
+  def setUp(self):
+    self.services = service_manager.Services(
+        project=fake.ProjectService(),
+        user_star=fake.UserStarService())
+
+    self.user_id = 2
+    self.project_id = 987
+    self.project = self.services.project.TestAddProject(
+        'proj', project_id=self.project_id)
+
+    self.mr = testing_helpers.MakeMonorailRequest(
+        services=self.services, project=self.project)
+    self.mr.cnxn = 'fake cnxn'
+    self.mr.viewed_user_auth.user_id = 100
+
+    self.mox = mox.Mox()
+
+  def tearDown(self):
+    self.mox.UnsetStubs()
+    self.mox.ResetAll()
+
+  def testUserUpdatesProjects(self):
+    uup = userupdates.UserUpdatesProjects(None, None, self.services)
+
+    self.mox.StubOutWithMock(sitewide_helpers, 'GetViewableStarredProjects')
+    sitewide_helpers.GetViewableStarredProjects(
+        self.mr.cnxn, self.services, self.mr.viewed_user_auth.user_id,
+        self.mr.auth.effective_ids, self.mr.auth.user_pb).AndReturn(
+            [self.project])
+
+    self.mox.StubOutWithMock(activities, 'GatherUpdatesData')
+    activities.GatherUpdatesData(
+        self.services, self.mr, user_ids=None,
+        project_ids=[self.project_id],
+        ending=uup._ENDING,
+        updates_page_url=uup._UPDATES_PAGE_URL,
+        highlight=uup._HIGHLIGHT).AndReturn({})
+
+    self.mox.ReplayAll()
+
+    page_data = uup.GatherPageData(self.mr)
+    self.mox.VerifyAll()
+    self.assertEqual(3, len(page_data))
+    self.assertEqual('st5', page_data['user_tab_mode'])
+    self.assertEqual('yes', page_data['viewing_user_page'])
+    self.assertEqual(uup._TAB_MODE, page_data['user_updates_tab_mode'])
+
+  def testUserUpdatesDevelopers(self):
+    uud = userupdates.UserUpdatesDevelopers(None, None, self.services)
+
+    self.mox.StubOutWithMock(self.services.user_star, 'LookupStarredItemIDs')
+    self.services.user_star.LookupStarredItemIDs(
+        self.mr.cnxn, self.mr.viewed_user_auth.user_id).AndReturn(
+            [self.user_id])
+
+    self.mox.StubOutWithMock(activities, 'GatherUpdatesData')
+    activities.GatherUpdatesData(
+        self.services, self.mr, user_ids=[self.user_id],
+        project_ids=None, ending=uud._ENDING,
+        updates_page_url=uud._UPDATES_PAGE_URL,
+        highlight=uud._HIGHLIGHT).AndReturn({})
+
+    self.mox.ReplayAll()
+
+    page_data = uud.GatherPageData(self.mr)
+    self.mox.VerifyAll()
+    self.assertEqual(3, len(page_data))
+    self.assertEqual('st5', page_data['user_tab_mode'])
+    self.assertEqual('yes', page_data['viewing_user_page'])
+    self.assertEqual(uud._TAB_MODE, page_data['user_updates_tab_mode'])
+
+  def testUserUpdatesIndividual(self):
+    uui = userupdates.UserUpdatesIndividual(None, None, self.services)
+
+    self.mox.StubOutWithMock(activities, 'GatherUpdatesData')
+    activities.GatherUpdatesData(
+        self.services, self.mr,
+        user_ids=[self.mr.viewed_user_auth.user_id],
+        project_ids=None, ending=uui._ENDING,
+        updates_page_url=uui._UPDATES_PAGE_URL,
+        highlight=uui._HIGHLIGHT).AndReturn({})
+
+    self.mox.ReplayAll()
+
+    page_data = uui.GatherPageData(self.mr)
+    self.mox.VerifyAll()
+    self.assertEqual(3, len(page_data))
+    self.assertEqual('st5', page_data['user_tab_mode'])
+    self.assertEqual('yes', page_data['viewing_user_page'])
+    self.assertEqual(uui._TAB_MODE, page_data['user_updates_tab_mode'])
+