diff --git a/reviewboard/testing/testcase.py b/reviewboard/testing/testcase.py
index 955b036208dc8e50df4e4d525fad00a1cc2cd596..8ad30fd319a54382d635deb14c5aa4cb2640210a 100644
--- a/reviewboard/testing/testcase.py
+++ b/reviewboard/testing/testcase.py
@@ -234,7 +234,8 @@ class TestCase(DjbletsTestCase):
             diff=diff)
 
     def create_repository(self, with_local_site=False, name='Test Repo',
-                          tool_name='Git', path=None, **kwargs):
+                          tool_name='Git', path=None, local_site=None,
+                          **kwargs):
         """Creates a Repository for testing.
 
         The Repository may optionally be attached to a LocalSite. It's also
@@ -244,10 +245,11 @@ class TestCase(DjbletsTestCase):
         The correct bundled repository path will be used for the given
         tool_name.
         """
-        if with_local_site:
-            local_site = LocalSite.objects.get(name=self.local_site_name)
-        else:
-            local_site = None
+        if not local_site:
+            if with_local_site:
+                local_site = LocalSite.objects.get(name=self.local_site_name)
+            else:
+                local_site = None
 
         testdata_dir = os.path.join(os.path.dirname(scmtools.__file__),
                                     'testdata')
diff --git a/reviewboard/webapi/tests/base.py b/reviewboard/webapi/tests/base.py
index 987246bfa5d19b97bd4f0a10c305fa70c5df37fd..23aa2272ea72b0b5d1d7d582797155a4af4ebe1d 100644
--- a/reviewboard/webapi/tests/base.py
+++ b/reviewboard/webapi/tests/base.py
@@ -30,6 +30,8 @@ from reviewboard.webapi.tests.urls import (
 class BaseWebAPITestCase(TestCase, EmailTestHelper):
     sample_api_url = None
 
+    error_mimetype = error_mimetype
+
     def setUp(self):
         super(BaseWebAPITestCase, self).setUp()
 
@@ -86,7 +88,7 @@ class BaseWebAPITestCase(TestCase, EmailTestHelper):
             self.assertEqual(expected_mimetype, None)
 
             if expected_status != 405:
-                self.assertEqual(response['Content-Type'], error_mimetype)
+                self.assertEqual(response['Content-Type'], self.error_mimetype)
         elif expected_status != 302:
             self.assertNotEqual(expected_mimetype, None)
             self.assertEqual(response['Content-Type'], expected_mimetype)
@@ -142,7 +144,7 @@ class BaseWebAPITestCase(TestCase, EmailTestHelper):
             self.assertEqual(expected_mimetype, None)
 
             if expected_status != 405:
-                self.assertEqual(response['Content-Type'], error_mimetype)
+                self.assertEqual(response['Content-Type'], self.error_mimetype)
         else:
             self.assertNotEqual(expected_mimetype, None)
             self.assertEqual(response['Content-Type'], expected_mimetype)
diff --git a/reviewboard/webapi/tests/mixins.py b/reviewboard/webapi/tests/mixins.py
index a5ee7d9e10e3011961a0df3c61aa53c21d261dd7..5e84c611755d380124aa3d0ab1e3c71afcb1757d 100644
--- a/reviewboard/webapi/tests/mixins.py
+++ b/reviewboard/webapi/tests/mixins.py
@@ -38,8 +38,12 @@ class BasicTestsMetaclass(type):
     The class can also set ``test_http_methods`` to a tuple of HTTP methods
     that should be tested. By default, this includes DELETE, GET, POST
     and PUT.
+
+    By default, tests will also be repeated on Local Sites. This can be
+    disabled by setting ``test_local_sites = False``.
     """
     def __new__(meta, name, bases, d):
+        test_local_sites = d.get('test_local_sites', True)
         resource = d['resource']
         is_singleton = False
         is_list = False
@@ -56,28 +60,48 @@ class BasicTestsMetaclass(type):
             is_singleton = True
 
         if 'DELETE' in test_http_methods and not is_list:
-            if 'DELETE' in resource.allowed_methods:
-                bases = (BasicDeleteTestsMixin,) + bases
+            if 'DELETE' not in resource.allowed_methods:
+                mixin = BasicDeleteNotAllowedTestsMixin
+            elif test_local_sites:
+                mixin = BasicDeleteTestsWithLocalSiteMixin
             else:
-                bases = (BasicDeleteNotAllowedTestsMixin,) + bases
+                mixin = BasicDeleteTestsMixin
+
+            bases = (mixin,) + bases
 
         if 'GET' in test_http_methods:
             if is_list:
-                bases = (BasicGetListTestsMixin,) + bases
+                if test_local_sites:
+                    mixin = BasicGetListTestsWithLocalSiteMixin
+                else:
+                    mixin = BasicGetListTestsMixin
             else:
-                bases = (BasicGetItemTestsMixin,) + bases
+                if test_local_sites:
+                    mixin = BasicGetItemTestsWithLocalSiteMixin
+                else:
+                    mixin = BasicGetItemTestsMixin
+
+            bases = (mixin,) + bases
 
         if 'POST' in test_http_methods and (is_list or is_singleton):
-            if 'POST' in resource.allowed_methods:
-                bases = (BasicPostTestsMixin,) + bases
+            if 'POST' not in resource.allowed_methods:
+                mixin = BasicPostNotAllowedTestsMixin
+            elif test_local_sites:
+                mixin = BasicPostTestsWithLocalSiteMixin
             else:
-                bases = (BasicPostNotAllowedTestsMixin,) + bases
+                mixin = BasicPostTestsMixin
+
+            bases = (mixin,) + bases
 
         if 'PUT' in test_http_methods and not is_list:
-            if 'PUT' in resource.allowed_methods:
-                bases = (BasicPutTestsMixin,) + bases
+            if 'PUT' not in resource.allowed_methods:
+                mixin = BasicPutNotAllowedTestsMixin
+            elif test_local_sites:
+                mixin = BasicPutTestsWithLocalSiteMixin
             else:
-                bases = (BasicPutNotAllowedTestsMixin,) + bases
+                mixin = BasicPutTestsMixin
+
+            bases = (mixin,) + bases
 
         return super(BasicTestsMetaclass, meta).__new__(meta, name, bases, d)
 
@@ -128,6 +152,28 @@ class BasicDeleteTestsMixin(BasicTestsMixin):
         self.apiDelete(url)
         self.check_delete_result(self.user, *cb_args)
 
+    @test_template
+    def test_delete_not_owner(self):
+        """Testing the DELETE <URL> API without owner"""
+        self.load_fixtures(self.basic_delete_fixtures)
+
+        user = User.objects.get(username='doc')
+        self.assertNotEqual(user, self.user)
+
+        url, cb_args = self.setup_basic_delete_test(user, False, None)
+        self.assertFalse(url.startswith('/s/' + self.local_site_name))
+
+        rsp = self.apiDelete(url, expected_status=403)
+        self.assertEqual(rsp['stat'], 'fail')
+        self.assertEqual(rsp['err']['code'], PERMISSION_DENIED.code)
+
+
+class BasicDeleteTestsWithLocalSiteMixin(BasicDeleteTestsMixin):
+    """Adds basic HTTP DELETE unit tests with Local Sites.
+
+    This extends BasicDeleteTestsMixin to also perform equivalent tests
+    on Local Sites.
+    """
     @add_fixtures(['test_site'])
     @test_template
     def test_delete_with_site(self):
@@ -160,21 +206,6 @@ class BasicDeleteTestsMixin(BasicTestsMixin):
         self.assertEqual(rsp['stat'], 'fail')
         self.assertEqual(rsp['err']['code'], PERMISSION_DENIED.code)
 
-    @test_template
-    def test_delete_not_owner(self):
-        """Testing the DELETE <URL> API without owner"""
-        self.load_fixtures(self.basic_delete_fixtures)
-
-        user = User.objects.get(username='doc')
-        self.assertNotEqual(user, self.user)
-
-        url, cb_args = self.setup_basic_delete_test(user, False, None)
-        self.assertFalse(url.startswith('/s/' + self.local_site_name))
-
-        rsp = self.apiDelete(url, expected_status=403)
-        self.assertEqual(rsp['stat'], 'fail')
-        self.assertEqual(rsp['err']['code'], PERMISSION_DENIED.code)
-
 
 class BasicDeleteNotAllowedTestsMixin(BasicTestsMixin):
     """Mixin to add HTTP 405 Not Allowed tests for HTTP DELETE.
@@ -225,6 +256,13 @@ class BasicGetItemTestsMixin(BasicTestsMixin):
         item_rsp = rsp[self.resource.item_result_key]
         self.compare_item(item_rsp, item)
 
+
+class BasicGetItemTestsWithLocalSiteMixin(BasicGetItemTestsMixin):
+    """Adds basic HTTP GET unit tests for item resources with Local Sites.
+
+    This extends BasicGetItemTestsMixin to also perform equivalent tests
+    on Local Sites.
+    """
     @add_fixtures(['test_site'])
     @test_template
     def test_get_with_site(self):
@@ -292,6 +330,13 @@ class BasicGetListTestsMixin(BasicTestsMixin):
         for i in range(len(items)):
             self.compare_item(items_rsp[i], items[i])
 
+
+class BasicGetListTestsWithLocalSiteMixin(BasicGetListTestsMixin):
+    """Adds basic HTTP GET unit tests for list resources with Local Sites.
+
+    This extends BasicGetListTestsMixin to also perform equivalent tests
+    on Local Sites.
+    """
     @add_fixtures(['test_site'])
     @test_template
     def test_get_with_site(self):
@@ -367,6 +412,13 @@ class BasicPostTestsMixin(BasicTestsMixin):
         self.assertEqual(rsp['stat'], 'ok')
         self.check_post_result(self.user, rsp, *cb_args)
 
+
+class BasicPostTestsWithLocalSiteMixin(BasicPostTestsMixin):
+    """Adds basic HTTP POST unit tests with Local Sites.
+
+    This extends BasicPostTestsMixin to also perform equivalent tests
+    on Local Sites.
+    """
     @add_fixtures(['test_site'])
     @test_template
     def test_post_with_site(self):
@@ -460,6 +512,29 @@ class BasicPutTestsMixin(BasicTestsMixin):
         self.check_put_result(self.user, rsp[self.resource.item_result_key],
                               item, *cb_args)
 
+    @test_template
+    def test_put_not_owner(self):
+        """Testing the PUT <URL> API without owner"""
+        self.load_fixtures(self.basic_put_fixtures)
+
+        user = User.objects.get(username='doc')
+        self.assertNotEqual(user, self.user)
+
+        url, mimetype, put_data, item, cb_args = \
+            self.setup_basic_put_test(user, False, None, False)
+        self.assertFalse(url.startswith('/s/' + self.local_site_name))
+
+        rsp = self.apiPut(url, put_data, expected_status=403)
+        self.assertEqual(rsp['stat'], 'fail')
+        self.assertEqual(rsp['err']['code'], PERMISSION_DENIED.code)
+
+
+class BasicPutTestsWithLocalSiteMixin(BasicPutTestsMixin):
+    """Adds basic HTTP PUT unit tests with Local Sites.
+
+    This extends BasicPutTestsMixin to also perform equivalent tests
+    on Local Sites.
+    """
     @add_fixtures(['test_site'])
     @test_template
     def test_put_with_site(self):
@@ -496,22 +571,6 @@ class BasicPutTestsMixin(BasicTestsMixin):
         self.assertEqual(rsp['stat'], 'fail')
         self.assertEqual(rsp['err']['code'], PERMISSION_DENIED.code)
 
-    @test_template
-    def test_put_not_owner(self):
-        """Testing the PUT <URL> API without owner"""
-        self.load_fixtures(self.basic_put_fixtures)
-
-        user = User.objects.get(username='doc')
-        self.assertNotEqual(user, self.user)
-
-        url, mimetype, put_data, item, cb_args = \
-            self.setup_basic_put_test(user, False, None, False)
-        self.assertFalse(url.startswith('/s/' + self.local_site_name))
-
-        rsp = self.apiPut(url, put_data, expected_status=403)
-        self.assertEqual(rsp['stat'], 'fail')
-        self.assertEqual(rsp['err']['code'], PERMISSION_DENIED.code)
-
 
 class BasicPutNotAllowedTestsMixin(BasicTestsMixin):
     """Mixin to add HTTP 405 Not Allowed tests for HTTP PUT.
