Merge "Add quota tests in api tests."
diff --git a/neutron/tests/tempest/api/base.py b/neutron/tests/tempest/api/base.py
index c1bd622..033ac67 100644
--- a/neutron/tests/tempest/api/base.py
+++ b/neutron/tests/tempest/api/base.py
@@ -13,11 +13,14 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import functools
+
 import netaddr
 from tempest.lib.common.utils import data_utils
 from tempest.lib import exceptions as lib_exc
 from tempest import test
 
+from neutron.common import constants
 from neutron.tests.tempest.api import clients
 from neutron.tests.tempest import config
 from neutron.tests.tempest import exceptions
@@ -460,3 +463,129 @@
         message = (
             "net(%s) has no usable IP address in allocation pools" % net_id)
         raise exceptions.InvalidConfiguration(message)
+
+
+def _require_sorting(f):
+    @functools.wraps(f)
+    def inner(self, *args, **kwargs):
+        if not CONF.neutron_plugin_options.validate_sorting:
+            self.skipTest('Sorting feature is required')
+        return f(self, *args, **kwargs)
+    return inner
+
+
+def _require_pagination(f):
+    @functools.wraps(f)
+    def inner(self, *args, **kwargs):
+        if not CONF.neutron_plugin_options.validate_pagination:
+            self.skipTest('Pagination feature is required')
+        return f(self, *args, **kwargs)
+    return inner
+
+
+class BaseSearchCriteriaTest(BaseNetworkTest):
+
+    # This should be defined by subclasses to reflect resource name to test
+    resource = None
+
+    # also test a case when there are multiple resources with the same name
+    resource_names = ('test1', 'abc1', 'test10', '123test') + ('test1',)
+
+    list_kwargs = {'shared': False}
+
+    force_tenant_isolation = True
+
+    @classmethod
+    def resource_setup(cls):
+        super(BaseSearchCriteriaTest, cls).resource_setup()
+
+        cls.create_method = getattr(cls, 'create_%s' % cls.resource)
+
+        # NOTE(ihrachys): some names, like those starting with an underscore
+        # (_) are sorted differently depending on whether the plugin implements
+        # native sorting support, or not. So we avoid any such cases here,
+        # sticking to alphanumeric.
+        for name in cls.resource_names:
+            args = {'%s_name' % cls.resource: name}
+            cls.create_method(**args)
+
+    def list_method(self, *args, **kwargs):
+        method = getattr(self.client, 'list_%ss' % self.resource)
+        kwargs.update(self.list_kwargs)
+        return method(*args, **kwargs)
+
+    @classmethod
+    def _extract_resources(cls, body):
+        return body['%ss' % cls.resource]
+
+    def _test_list_sorts(self, direction):
+        sort_args = {
+            'sort_dir': direction,
+            'sort_key': 'name'
+        }
+        body = self.list_method(**sort_args)
+        resources = self._extract_resources(body)
+        self.assertNotEmpty(
+            resources, "%s list returned is empty" % self.resource)
+        retrieved_names = [res['name'] for res in resources]
+        expected = sorted(retrieved_names)
+        if direction == constants.SORT_DIRECTION_DESC:
+            expected = list(reversed(expected))
+        self.assertEqual(expected, retrieved_names)
+
+    @_require_sorting
+    def _test_list_sorts_asc(self):
+        self._test_list_sorts(constants.SORT_DIRECTION_ASC)
+
+    @_require_sorting
+    def _test_list_sorts_desc(self):
+        self._test_list_sorts(constants.SORT_DIRECTION_DESC)
+
+    @_require_pagination
+    def _test_list_pagination(self):
+        for limit in range(1, len(self.resource_names) + 1):
+            pagination_args = {
+                'limit': limit,
+            }
+            body = self.list_method(**pagination_args)
+            resources = self._extract_resources(body)
+            self.assertEqual(limit, len(resources))
+
+    @_require_pagination
+    def _test_list_no_pagination_limit_0(self):
+        pagination_args = {
+            'limit': 0,
+        }
+        body = self.list_method(**pagination_args)
+        resources = self._extract_resources(body)
+        self.assertTrue(len(resources) >= len(self.resource_names))
+
+    @_require_pagination
+    @_require_sorting
+    def _test_list_pagination_with_marker(self):
+        # first, collect all resources for later comparison
+        sort_args = {
+            'sort_dir': constants.SORT_DIRECTION_ASC,
+            'sort_key': 'name'
+        }
+        body = self.list_method(**sort_args)
+        expected_resources = self._extract_resources(body)
+        self.assertNotEmpty(expected_resources)
+
+        # paginate resources one by one, using last fetched resource as a
+        # marker
+        resources = []
+        for i in range(len(expected_resources)):
+            pagination_args = sort_args.copy()
+            pagination_args['limit'] = 1
+            if resources:
+                pagination_args['marker'] = resources[-1]['id']
+            body = self.list_method(**pagination_args)
+            resources_ = self._extract_resources(body)
+            self.assertEqual(1, len(resources_))
+            resources.extend(resources_)
+
+        # finally, compare that the list retrieved in one go is identical to
+        # the one containing pagination results
+        for expected, res in zip(expected_resources, resources):
+            self.assertEqual(expected['name'], res['name'])
diff --git a/neutron/tests/tempest/api/test_networks.py b/neutron/tests/tempest/api/test_networks.py
index b96a5bd..4816d79 100644
--- a/neutron/tests/tempest/api/test_networks.py
+++ b/neutron/tests/tempest/api/test_networks.py
@@ -89,3 +89,33 @@
         self.assertNotEmpty(networks, "Network list returned is empty")
         for network in networks:
             self.assertEqual(sorted(network.keys()), sorted(fields))
+
+
+class NetworksSearchCriteriaTest(base.BaseSearchCriteriaTest):
+
+    resource = 'network'
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('de27d34a-bd9d-4516-83d6-81ef723f7d0d')
+    def test_list_sorts_asc(self):
+        self._test_list_sorts_asc()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('e767a160-59f9-4c4b-8dc1-72124a68640a')
+    def test_list_sorts_desc(self):
+        self._test_list_sorts_desc()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('71389852-f57b-49f2-b109-77b705e9e8af')
+    def test_list_pagination(self):
+        self._test_list_pagination()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('71389852-f57b-49f2-b109-77b705e9e8af')
+    def test_list_pagination_with_marker(self):
+        self._test_list_pagination_with_marker()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('f1867fc5-e1d6-431f-bc9f-8b882e43a7f9')
+    def test_list_no_pagination_limit_0(self):
+        self._test_list_no_pagination_limit_0()
diff --git a/neutron/tests/tempest/api/test_networks_negative.py b/neutron/tests/tempest/api/test_networks_negative.py
new file mode 100644
index 0000000..87da1b8
--- /dev/null
+++ b/neutron/tests/tempest/api/test_networks_negative.py
@@ -0,0 +1,36 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.lib import exceptions as lib_exc
+from tempest import test
+import testtools
+
+from neutron.tests.tempest.api import base
+
+
+class NetworksNegativeTest(base.BaseNetworkTest):
+
+    @classmethod
+    def resource_setup(cls):
+        super(NetworksNegativeTest, cls).resource_setup()
+        cls.network = cls.create_network()
+        cls.subnet = cls.create_subnet(cls.network)
+
+    @test.attr(type='negative')
+    @test.idempotent_id('9f80f25b-5d1b-4f26-9f6b-774b9b270819')
+    def test_delete_network_in_use(self):
+        port = self.client.create_port(network_id=self.network['id'])
+        self.addCleanup(self.client.delete_port, port['port']['id'])
+        with testtools.ExpectedException(lib_exc.Conflict):
+            self.client.delete_subnet(self.subnet['id'])
+        with testtools.ExpectedException(lib_exc.Conflict):
+            self.client.delete_network(self.network['id'])
diff --git a/neutron/tests/tempest/api/test_ports.py b/neutron/tests/tempest/api/test_ports.py
index dd929da..7bf4790 100644
--- a/neutron/tests/tempest/api/test_ports.py
+++ b/neutron/tests/tempest/api/test_ports.py
@@ -39,3 +39,10 @@
         self.assertEqual('d2', body['port']['description'])
         body = self.client.list_ports(id=body['port']['id'])['ports'][0]
         self.assertEqual('d2', body['description'])
+
+    @test.idempotent_id('c72c1c0c-2193-4aca-bbb4-b1442640c123')
+    def test_change_dhcp_flag_then_create_port(self):
+        s = self.create_subnet(self.network, enable_dhcp=False)
+        self.create_port(self.network)
+        self.client.update_subnet(s['id'], enable_dhcp=True)
+        self.create_port(self.network)
diff --git a/neutron/tests/tempest/api/test_routers_negative.py b/neutron/tests/tempest/api/test_routers_negative.py
index 2ccb34a..6a028db 100644
--- a/neutron/tests/tempest/api/test_routers_negative.py
+++ b/neutron/tests/tempest/api/test_routers_negative.py
@@ -21,20 +21,41 @@
 from neutron.tests.tempest.api import base_routers as base
 
 
-class DvrRoutersNegativeTest(base.BaseRouterTest):
+class RoutersNegativeTestBase(base.BaseRouterTest):
+
+    @classmethod
+    def resource_setup(cls):
+        super(RoutersNegativeTestBase, cls).resource_setup()
+        cls.router = cls.create_router(data_utils.rand_name('router'))
+        cls.network = cls.create_network()
+        cls.subnet = cls.create_subnet(cls.network)
+
+
+class RoutersNegativeTest(RoutersNegativeTestBase):
+
+    @classmethod
+    @test.requires_ext(extension="router", service="network")
+    def skip_checks(cls):
+        super(RoutersNegativeTest, cls).skip_checks()
+
+    @test.attr(type='negative')
+    @test.idempotent_id('e3e751af-15a2-49cc-b214-a7154579e94f')
+    def test_delete_router_in_use(self):
+        # This port is deleted after a test by remove_router_interface.
+        port = self.client.create_port(network_id=self.network['id'])
+        self.client.add_router_interface_with_port_id(
+            self.router['id'], port['port']['id'])
+        with testtools.ExpectedException(lib_exc.Conflict):
+            self.client.delete_router(self.router['id'])
+
+
+class DvrRoutersNegativeTest(RoutersNegativeTestBase):
 
     @classmethod
     @test.requires_ext(extension="dvr", service="network")
     def skip_checks(cls):
         super(DvrRoutersNegativeTest, cls).skip_checks()
 
-    @classmethod
-    def resource_setup(cls):
-        super(DvrRoutersNegativeTest, cls).resource_setup()
-        cls.router = cls.create_router(data_utils.rand_name('router'))
-        cls.network = cls.create_network()
-        cls.subnet = cls.create_subnet(cls.network)
-
     @test.attr(type='negative')
     @test.idempotent_id('4990b055-8fc7-48ab-bba7-aa28beaad0b9')
     def test_router_create_tenant_distributed_returns_forbidden(self):
diff --git a/neutron/tests/tempest/config.py b/neutron/tests/tempest/config.py
index f50fa39..bad1000 100644
--- a/neutron/tests/tempest/config.py
+++ b/neutron/tests/tempest/config.py
@@ -12,6 +12,7 @@
 
 from oslo_config import cfg
 
+from neutron import api
 from tempest import config
 
 
@@ -22,7 +23,13 @@
     cfg.BoolOpt('specify_floating_ip_address_available',
                 default=True,
                 help='Allow passing an IP Address of the floating ip when '
-                     'creating the floating ip')]
+                     'creating the floating ip'),
+    cfg.BoolOpt('validate_pagination',
+                default=api.DEFAULT_ALLOW_PAGINATION,
+                help='Validate pagination'),
+    cfg.BoolOpt('validate_sorting',
+                default=api.DEFAULT_ALLOW_SORTING,
+                help='Validate sorting')]
 
 # TODO(amuller): Redo configuration options registration as part of the planned
 # transition to the Tempest plugin architecture