Merge "Add negative testcases for ownership transfer_request and tsigkey"
diff --git a/designate_tempest_plugin/clients.py b/designate_tempest_plugin/clients.py
index 5f4f728..d85d343 100644
--- a/designate_tempest_plugin/clients.py
+++ b/designate_tempest_plugin/clients.py
@@ -13,7 +13,9 @@
 # under the License.
 from tempest import clients
 from tempest import config
+from tempest.lib.auth import KeystoneAuthProvider
 from tempest.lib.auth import KeystoneV2AuthProvider
+from tempest.lib.auth import KeystoneV3AuthProvider
 
 from designate_tempest_plugin.services.dns.v1.json.domains_client import \
     DomainsClient
@@ -27,7 +29,7 @@
     ZoneImportsClient
 from designate_tempest_plugin.services.dns.v2.json.blacklists_client import \
     BlacklistsClient
-from designate_tempest_plugin.services.dns.admin.json.quotas_client import \
+from designate_tempest_plugin.services.dns.v2.json.quotas_client import \
     QuotasClient
 from designate_tempest_plugin.services.dns.v2.json.zone_exports_client import \
     ZoneExportsClient
@@ -37,6 +39,8 @@
     PoolClient
 from designate_tempest_plugin.services.dns.v2.json.tld_client import \
     TldClient
+from designate_tempest_plugin.services.dns.admin.json.quotas_client import \
+    QuotasClient as AdminQuotaClient
 from designate_tempest_plugin.services.dns.query.query_client import \
     QueryClient
 from designate_tempest_plugin.services.dns.v2.json.transfer_request_client \
@@ -112,11 +116,33 @@
         return params
 
 
+class ManagerAdmin(clients.Manager):
+
+    def __init__(self, credentials=None, service=None):
+        super(ManagerAdmin, self).__init__(credentials, service)
+        self._init_clients(self._get_params())
+
+    def _init_clients(self, params):
+        self.quotas_client = AdminQuotaClient(**params)
+
+    def _get_params(self):
+        params = dict(self.default_params)
+        params.update({
+            'auth_provider': self.auth_provider,
+            'service': CONF.dns.catalog_type,
+            'region': CONF.identity.region,
+            'endpoint_type': CONF.dns.endpoint_type,
+            'build_interval': CONF.dns.build_interval,
+            'build_timeout': CONF.dns.build_timeout
+        })
+        return params
+
+
 class ManagerV2Unauthed(ManagerV2):
 
     def __init__(self, credentials=None, service=None):
         super(ManagerV2Unauthed, self).__init__(credentials, service)
-        self.auth_provider = KeystoneV2UnauthedProvider(
+        self.auth_provider = self._auth_provider_class()(
             credentials=self.auth_provider.credentials,
             auth_url=self.auth_provider.auth_client.auth_url,
             disable_ssl_certificate_validation=self.auth_provider.dscv,
@@ -125,13 +151,18 @@
         )
         self._init_clients(self._get_params())
 
+    def _auth_provider_class(self):
+        if CONF.identity.auth_version == 'v3':
+            return KeystoneV3UnauthedProvider
+        else:
+            return KeystoneV2UnauthedProvider
 
-class KeystoneV2UnauthedProvider(KeystoneV2AuthProvider):
-    """This auth provider will ensure requests are made without a token"""
+
+class BaseUnauthedProvider(KeystoneAuthProvider):
 
     def _decorate_request(self, filters, method, url, headers=None, body=None,
                           auth_data=None):
-        result = super(KeystoneV2UnauthedProvider, self)._decorate_request(
+        result = super(BaseUnauthedProvider, self)._decorate_request(
             filters, method, url, headers=headers, body=body,
             auth_data=auth_data)
         url, headers, body = result
@@ -140,3 +171,15 @@
         except KeyError:
             pass
         return url, headers, body
+
+
+class KeystoneV2UnauthedProvider(KeystoneV2AuthProvider, BaseUnauthedProvider):
+
+    def _decorate_request(self, *args, **kwargs):
+        return BaseUnauthedProvider._decorate_request(self, *args, **kwargs)
+
+
+class KeystoneV3UnauthedProvider(KeystoneV3AuthProvider, BaseUnauthedProvider):
+
+    def _decorate_request(self, *args, **kwargs):
+        return BaseUnauthedProvider._decorate_request(self, *args, **kwargs)
diff --git a/designate_tempest_plugin/config.py b/designate_tempest_plugin/config.py
index 5b99e81..e4e0270 100644
--- a/designate_tempest_plugin/config.py
+++ b/designate_tempest_plugin/config.py
@@ -71,4 +71,11 @@
     cfg.BoolOpt('api_v2_root_recordsets',
                 default=False,
                 help="Is the v2 root recordsets API enabled."),
+    cfg.BoolOpt('api_v2_quotas',
+                default=False,
+                help="Is the v2 quota API enabled."),
+    cfg.BoolOpt('bug_1573141_fixed',
+                default=False,
+                help="Is https://bugs.launchpad.net/designate/+bug/1573141 "
+                "fixed"),
 ]
diff --git a/designate_tempest_plugin/data_utils.py b/designate_tempest_plugin/data_utils.py
index 11113b8..7600542 100644
--- a/designate_tempest_plugin/data_utils.py
+++ b/designate_tempest_plugin/data_utils.py
@@ -81,24 +81,26 @@
 
 def rand_quotas(zones=None, zone_records=None, zone_recordsets=None,
                 recordset_records=None, api_export_size=None):
-    LOG.warn("Leaving `api_export_size` out of quota data due to: "
-             "https://bugs.launchpad.net/designate/+bug/1573141")
-    return {
-        'quota': {
-            'zones':
-                zones or data_utils.rand_int_id(100, 999999),
-            'zone_records':
-                zone_records or data_utils.rand_int_id(100, 999999),
-            'zone_recordsets':
-                zone_recordsets or data_utils.rand_int_id(100, 999999),
-            'recordset_records':
-                recordset_records or data_utils.rand_int_id(100, 999999),
-            # https://bugs.launchpad.net/designate/+bug/1573141
-            # 'api_export_size':
-            #     api_export_size or data_utils.rand_int_id(100, 999999),
-        }
+    quotas_dict = {
+        'zones':
+            zones or data_utils.rand_int_id(100, 999999),
+        'zone_records':
+            zone_records or data_utils.rand_int_id(100, 999999),
+        'zone_recordsets':
+            zone_recordsets or data_utils.rand_int_id(100, 999999),
+        'recordset_records':
+            recordset_records or data_utils.rand_int_id(100, 999999),
     }
 
+    if CONF.dns_feature_enabled.bug_1573141_fixed:
+        quotas_dict['api_export_size'] = \
+            api_export_size or data_utils.rand_int_id(100, 999999)
+    else:
+        LOG.warn("Leaving `api_export_size` out of quota data due to: "
+                 "https://bugs.launchpad.net/designate/+bug/1573141")
+
+    return quotas_dict
+
 
 def rand_zone_data(name=None, email=None, ttl=None, description=None):
     """Generate random zone data, with optional overrides
diff --git a/designate_tempest_plugin/services/dns/admin/json/quotas_client.py b/designate_tempest_plugin/services/dns/admin/json/quotas_client.py
index c815611..08b1790 100644
--- a/designate_tempest_plugin/services/dns/admin/json/quotas_client.py
+++ b/designate_tempest_plugin/services/dns/admin/json/quotas_client.py
@@ -53,7 +53,9 @@
             api_export_size=api_export_size,
         )
 
-        resp, body = self._update_request('quotas', project_id, quotas,
+        quotas_dict = {'quota': quotas}
+
+        resp, body = self._update_request('quotas', project_id, quotas_dict,
                                           params=params)
 
         self.expected_success(200, resp.status)
diff --git a/designate_tempest_plugin/services/dns/json/base.py b/designate_tempest_plugin/services/dns/json/base.py
index ea8fb8f..19dbe45 100644
--- a/designate_tempest_plugin/services/dns/json/base.py
+++ b/designate_tempest_plugin/services/dns/json/base.py
@@ -129,17 +129,24 @@
 
         return resp, self.deserialize(resp, body)
 
-    def _show_request(self, resource, uuid, headers=None, params=None):
+    def _show_request(self, resource, uuid, headers=None, params=None,
+                      extra_headers=False):
         """Gets a specific object of the specified type.
         :param resource: The name of the REST resource, e.g., 'zones'.
         :param uuid: Unique identifier of the object in UUID format.
         :param params: A Python dict that represents the query paramaters to
                        include in the request URI.
+        :param extra_headers (bool): Boolean value than indicates if the
+                                     headers returned by the get_headers()
+                                     method are to be used but additional
+                                     headers are needed in the request
+                                     pass them in as a dict.
         :returns: Serialized object as a dictionary.
         """
         uri = self.get_uri(resource, uuid=uuid, params=params)
 
-        resp, body = self.get(uri, headers=headers)
+        resp, body = self.get(
+            uri, headers=headers, extra_headers=extra_headers)
 
         self.expected_success(self.SHOW_STATUS_CODES, resp.status)
 
@@ -179,7 +186,8 @@
 
         return resp, self.deserialize(resp, body)
 
-    def _update_request(self, resource, uuid, data, params=None):
+    def _update_request(self, resource, uuid, data, params=None, headers=None,
+                        extra_headers=False):
         """Updates the specified object using PATCH request.
         :param resource: The name of the REST resource, e.g., 'zones'
         :param uuid: Unique identifier of the object in UUID format.
@@ -188,28 +196,44 @@
                      is sent as-is.
         :param params: A Python dict that represents the query paramaters to
                        include in the request URI.
+        :param headers (dict): The headers to use for the request.
+        :param extra_headers (bool): Boolean value than indicates if the
+                                     headers returned by the get_headers()
+                                     method are to be used but additional
+                                     headers are needed in the request
+                                     pass them in as a dict.
         :returns: Serialized object as a dictionary.
         """
         body = self.serialize(data)
         uri = self.get_uri(resource, uuid=uuid, params=params)
 
-        resp, body = self.patch(uri, body=body)
+        resp, body = self.patch(uri, body=body,
+                                headers=headers, extra_headers=True)
 
         self.expected_success(self.UPDATE_STATUS_CODES, resp.status)
 
         return resp, self.deserialize(resp, body)
 
-    def _delete_request(self, resource, uuid, params=None):
+    def _delete_request(self, resource, uuid, params=None, headers=None,
+                        extra_headers=False):
         """Deletes the specified object.
         :param resource: The name of the REST resource, e.g., 'zones'.
         :param uuid: The unique identifier of an object in UUID format.
         :param params: A Python dict that represents the query paramaters to
                        include in the request URI.
+        :param headers (dict): The headers to use for the request.
+        :param extra_headers (bool): Boolean value than indicates if the
+                                     headers returned by the get_headers()
+                                     method are to be used but additional
+                                     headers are needed in the request
+                                     pass them in as a dict.
         :returns: A tuple with the server response and the response body.
         """
         uri = self.get_uri(resource, uuid=uuid, params=params)
 
-        resp, body = self.delete(uri)
+        resp, body = self.delete(
+            uri, headers=headers, extra_headers=extra_headers)
+
         self.expected_success(self.DELETE_STATUS_CODES, resp.status)
         if resp.status == 202:
             body = self.deserialize(resp, body)
diff --git a/designate_tempest_plugin/services/dns/v2/json/quotas_client.py b/designate_tempest_plugin/services/dns/v2/json/quotas_client.py
new file mode 100644
index 0000000..3522a1d
--- /dev/null
+++ b/designate_tempest_plugin/services/dns/v2/json/quotas_client.py
@@ -0,0 +1,101 @@
+# Copyright 2016 Rackspace
+#
+# 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 oslo_log import log as logging
+
+from designate_tempest_plugin import data_utils as dns_data_utils
+from designate_tempest_plugin.services.dns.v2.json import base
+
+LOG = logging.getLogger(__name__)
+
+
+class QuotasClient(base.DnsClientV2Base):
+
+    @base.handle_errors
+    def update_quotas(self, zones=None, zone_records=None,
+                      zone_recordsets=None, recordset_records=None,
+                      api_export_size=None, project_id=None, params=None,
+                      headers=None):
+        """Update the quotas for the project id
+
+        :param zones: The limit on zones per tenant
+            Default: Random Value
+        :param zone_records: The limit on records per zone
+            Default: Random Value
+        :param zone_recordsets: The limit recordsets per zone
+            Default: Random Value
+        :param recordset_records: The limit on records per recordset
+            Default: Random Value
+        :param api_export_size: The limit on size of on exported zone
+            Default: Random Value
+        :param project_id: Apply the quotas to this project id
+            Default: The project id of this client
+        :param params: A Python dict that represents the query paramaters to
+                       include in the request URI.
+        :param headers (dict): The headers to use for the request.
+        :return: A tuple with the server response and the created quota.
+        """
+        project_id = project_id or self.tenant_id
+
+        quotas = dns_data_utils.rand_quotas(
+            zones=zones,
+            zone_records=zone_records,
+            zone_recordsets=zone_recordsets,
+            recordset_records=recordset_records,
+            api_export_size=api_export_size,
+        )
+
+        resp, body = self._update_request('quotas', project_id, quotas,
+                                          params=params, headers=headers,
+                                          extra_headers=True)
+
+        self.expected_success(200, resp.status)
+
+        return resp, body
+
+    @base.handle_errors
+    def show_quotas(self, project_id=None, params=None, headers=None):
+        """Gets a specific quota.
+
+        :param project_id: Show the quotas of this project id
+            Default: The project id of this client
+        :param params: A Python dict that represents the query paramaters to
+                       include in the request URI.
+        :param headers (dict): The headers to use for the request.
+        :return: Serialized quota as a dictionary.
+        """
+        project_id = project_id or self.tenant_id
+        return self._show_request('quotas', project_id, params=params,
+                                  headers=headers, extra_headers=True)
+
+    @base.handle_errors
+    def delete_quotas(self, project_id=None, params=None, headers=None):
+        """Resets the quotas for the specified project id
+
+        :param project_id: Reset the quotas of this project id
+            Default: The project id of this client
+        :param params: A Python dict that represents the query paramaters to
+                       include in the request URI.
+        :param headers (dict): The headers to use for the request.
+        :return: A tuple with the server response and the response body.
+        """
+        project_id = project_id or self.tenant_id
+
+        resp, body = self._delete_request(
+            'quotas', project_id,
+            params=params, headers=headers,
+            extra_headers=True)
+
+        self.expected_success(204, resp.status)
+
+        return resp, body
diff --git a/designate_tempest_plugin/tests/api/admin/test_quotas.py b/designate_tempest_plugin/tests/api/admin/test_quotas.py
index 762f817..ae49460 100644
--- a/designate_tempest_plugin/tests/api/admin/test_quotas.py
+++ b/designate_tempest_plugin/tests/api/admin/test_quotas.py
@@ -12,17 +12,24 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 from oslo_log import log as logging
+from tempest import config
 from tempest.lib import decorators
 
 from designate_tempest_plugin.tests import base
 from designate_tempest_plugin import data_utils as dns_data_utils
 
 LOG = logging.getLogger(__name__)
+CONF = config.CONF
 
 
 class BaseQuotasTest(base.BaseDnsAdminTest):
-    # see: https://bugs.launchpad.net/designate/+bug/1573141
-    excluded_keys = ['api_expected_size']
+
+    excluded_keys = []
+
+    def setUp(self):
+        if CONF.dns_feature_enabled.bug_1573141_fixed:
+            self.excluded_keys = ['api_export_size']
+        super(BaseQuotasTest, self).setUp()
 
 
 class QuotasAdminTest(BaseQuotasTest):
@@ -39,14 +46,14 @@
     def test_show_quotas(self):
         LOG.info("Updating quotas")
         quotas = dns_data_utils.rand_quotas()
-        _, body = self.admin_client.update_quotas(**quotas['quota'])
+        _, body = self.admin_client.update_quotas(**quotas)
         self.addCleanup(self.admin_client.delete_quotas)
 
         LOG.info("Fetching quotas")
         _, body = self.admin_client.show_quotas()
 
         LOG.info("Ensuring the response has all quota types")
-        self.assertExpected(quotas['quota'], body['quota'], self.excluded_keys)
+        self.assertExpected(quotas, body['quota'], self.excluded_keys)
 
     @decorators.idempotent_id('33e0affb-5d66-4216-881c-f101a779851a')
     def test_delete_quotas(self):
@@ -60,8 +67,8 @@
     def test_update_quotas(self):
         LOG.info("Updating quotas")
         quotas = dns_data_utils.rand_quotas()
-        _, body = self.admin_client.update_quotas(**quotas['quota'])
+        _, body = self.admin_client.update_quotas(**quotas)
         self.addCleanup(self.admin_client.delete_quotas)
 
         LOG.info("Ensuring the response has all quota types")
-        self.assertExpected(quotas['quota'], body['quota'], self.excluded_keys)
+        self.assertExpected(quotas, body['quota'], self.excluded_keys)
diff --git a/designate_tempest_plugin/tests/api/v2/test_quotas.py b/designate_tempest_plugin/tests/api/v2/test_quotas.py
new file mode 100644
index 0000000..9411270
--- /dev/null
+++ b/designate_tempest_plugin/tests/api/v2/test_quotas.py
@@ -0,0 +1,124 @@
+# Copyright 2016 Rackspace
+#
+# 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 oslo_log import log as logging
+from tempest import config
+from tempest.lib import decorators
+
+from designate_tempest_plugin.tests import base
+from designate_tempest_plugin import data_utils as dns_data_utils
+
+LOG = logging.getLogger(__name__)
+
+
+CONF = config.CONF
+
+
+class QuotasV2Test(base.BaseDnsV2Test):
+
+    credentials = ['primary', 'admin']
+
+    @classmethod
+    def skip_checks(cls):
+        super(QuotasV2Test, cls).skip_checks()
+
+        if not CONF.dns_feature_enabled.api_v2_quotas:
+            skip_msg = ("%s skipped as designate V2 Quotas API is not "
+                        "available" % cls.__name__)
+            raise cls.skipException(skip_msg)
+
+    @classmethod
+    def setup_clients(cls):
+        super(QuotasV2Test, cls).setup_clients()
+
+        cls.quotas_client = cls.os.quotas_client
+        cls.admin_client = cls.os_adm.quotas_client
+
+    @decorators.idempotent_id('1dac991a-9e2e-452c-a47a-26ac37381ec5')
+    def test_show_quotas(self):
+        LOG.info("Updating quotas")
+        quotas = dns_data_utils.rand_quotas()
+        _, body = self.admin_client.update_quotas(**quotas)
+        self.addCleanup(self.admin_client.delete_quotas)
+
+        LOG.info("Fetching quotas")
+        _, body = self.admin_client.show_quotas()
+
+        LOG.info("Ensuring the response has all quota types")
+        self.assertExpected(quotas, body, [])
+
+    @decorators.idempotent_id('0448b089-5803-4ce3-8a6c-5c15ff75a2cc')
+    def test_delete_quotas(self):
+        LOG.info("Deleting quotas")
+        _, body = self.admin_client.delete_quotas()
+
+        LOG.info("Ensuring an empty response body")
+        self.assertEqual(body.strip(), "")
+
+    @decorators.idempotent_id('76d24c87-1b39-4e19-947c-c08e1380dc61')
+    def test_update_quotas(self):
+        LOG.info("Updating quotas")
+        quotas = dns_data_utils.rand_quotas()
+        _, body = self.admin_client.update_quotas(**quotas)
+        self.addCleanup(self.admin_client.delete_quotas)
+
+        LOG.info("Ensuring the response has all quota types")
+        self.assertExpected(quotas, body, [])
+
+    @decorators.idempotent_id('76d24c87-1b39-4e19-947c-c08e1380dc61')
+    def test_update_quotas_other_project(self):
+
+        project_id = self.quotas_client.tenant_id
+
+        LOG.info("Updating quotas for %s ", project_id)
+
+        quotas = dns_data_utils.rand_quotas()
+        request = quotas.copy()
+        request['project_id'] = project_id
+        request['headers'] = {'X-Auth-All-Projects': True}
+        _, body = self.admin_client.update_quotas(**request)
+        self.addCleanup(self.admin_client.delete_quotas, project_id=project_id)
+
+        LOG.info("Ensuring the response has all quota types")
+        self.assertExpected(quotas, body, [])
+
+        _, client_body = self.quotas_client.show_quotas()
+
+        self.assertExpected(quotas, client_body, [])
+
+    @decorators.idempotent_id('21e45d30-dbc1-4173-9d6b-9b6813ef514b')
+    def test_reset_quotas_other_project(self):
+
+        # Use a fake project for this
+        project_id = '21e45d30-dbc1-4173-9d6b-9b6813ef514b'
+
+        _, original_quotas = self.admin_client.show_quotas(
+            project_id=project_id, headers={'X-Auth-All-Projects': True})
+
+        LOG.info("Updating quotas for %s ", project_id)
+
+        quotas = dns_data_utils.rand_quotas()
+        request = quotas.copy()
+        request['project_id'] = project_id
+        request['headers'] = {'X-Auth-All-Projects': True}
+        _, body = self.admin_client.update_quotas(**request)
+        self.addCleanup(self.admin_client.delete_quotas, project_id=project_id)
+
+        self.admin_client.delete_quotas(
+            project_id=project_id,
+            headers={'X-Auth-All-Projects': True})
+
+        _, final_quotas = self.admin_client.show_quotas(
+            project_id=project_id, headers={'X-Auth-All-Projects': True})
+
+        self.assertExpected(original_quotas, final_quotas, [])
diff --git a/designate_tempest_plugin/tests/base.py b/designate_tempest_plugin/tests/base.py
index 9f24d62..c86b38a 100644
--- a/designate_tempest_plugin/tests/base.py
+++ b/designate_tempest_plugin/tests/base.py
@@ -85,8 +85,8 @@
 class BaseDnsAdminTest(BaseDnsTest):
     """Base class for DNS Admin API tests."""
 
-    # Use the Designate V2 Client Manager
-    client_manager = clients.ManagerV2
+    # Use the Designate Admin Client Manager
+    client_manager = clients.ManagerAdmin
 
     @classmethod
     def skip_checks(cls):