Merge "Create "A" type recordset using a list of IPs records"
diff --git a/designate_tempest_plugin/services/dns/v2/__init__.py b/designate_tempest_plugin/services/dns/v2/__init__.py
index 2f738df..44cc403 100644
--- a/designate_tempest_plugin/services/dns/v2/__init__.py
+++ b/designate_tempest_plugin/services/dns/v2/__init__.py
@@ -39,9 +39,11 @@
     ZoneExportsClient
 from designate_tempest_plugin.services.dns.v2.json.zone_imports_client import \
     ZoneImportsClient
+from designate_tempest_plugin.services.dns.v2.json.api_version_client import \
+    ApiVersionClient
 
 __all__ = ['BlacklistsClient', 'DesignateLimitClient', 'PoolClient',
            'PtrClient', 'QuotasClient', 'RecordsetClient', 'ServiceClient',
            'TldClient', 'TransferAcceptClient', 'TransferRequestClient',
            'TsigkeyClient', 'ZonesClient', 'ZoneExportsClient',
-           'ZoneImportsClient']
+           'ZoneImportsClient', 'ApiVersionClient']
diff --git a/designate_tempest_plugin/services/dns/v2/json/api_version_client.py b/designate_tempest_plugin/services/dns/v2/json/api_version_client.py
new file mode 100644
index 0000000..5a78540
--- /dev/null
+++ b/designate_tempest_plugin/services/dns/v2/json/api_version_client.py
@@ -0,0 +1,27 @@
+# Copyright 2021 Red Hat.
+#
+# 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 designate_tempest_plugin.services.dns.v2.json import base
+
+
+class ApiVersionClient(base.DnsClientV2Base):
+
+    @base.handle_errors
+    def list_enabled_api_versions(self):
+        """Show all enabled API versions
+
+        :return: Dictionary containing version details
+        """
+        resp, body = self.get('/')
+        self.expected_success(self.LIST_STATUS_CODES, resp.status)
+        return resp, self.deserialize(resp, body)
diff --git a/designate_tempest_plugin/services/dns/v2/json/blacklists_client.py b/designate_tempest_plugin/services/dns/v2/json/blacklists_client.py
index c3f6de1..bfe65c7 100644
--- a/designate_tempest_plugin/services/dns/v2/json/blacklists_client.py
+++ b/designate_tempest_plugin/services/dns/v2/json/blacklists_client.py
@@ -36,6 +36,9 @@
             'description': description or data_utils.rand_name(),
         }
 
+        if pattern == '':
+            blacklist['pattern'] = ''
+
         resp, body = self._create_request('blacklists', blacklist,
                                           params=params)
 
diff --git a/designate_tempest_plugin/services/dns/v2/json/quotas_client.py b/designate_tempest_plugin/services/dns/v2/json/quotas_client.py
index f9ee10a..97398c0 100644
--- a/designate_tempest_plugin/services/dns/v2/json/quotas_client.py
+++ b/designate_tempest_plugin/services/dns/v2/json/quotas_client.py
@@ -88,3 +88,25 @@
         self.expected_success(204, resp.status)
 
         return resp, body
+
+    @base.handle_errors
+    def set_quotas(self, project_id, quotas, params=None, headers=None):
+        """Set a specific quota to given project ID
+
+        :param project_id: Set the quotas of this project id.
+        :param quotas: Dictionary of quotas to set (POST payload)
+        :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.
+        """
+        if headers is None:
+            headers = {'content-type': 'application/json'}
+        if 'content-type' not in [header.lower() for header in headers]:
+            headers['content-type'] = 'application/json'
+
+        resp, body = self._update_request(
+            "quotas", project_id,
+            data=quotas, params=params, headers=headers)
+        self.expected_success(200, resp.status)
+        return resp, body
diff --git a/designate_tempest_plugin/tests/api/v2/test_blacklists.py b/designate_tempest_plugin/tests/api/v2/test_blacklists.py
index d39536f..95688b3 100644
--- a/designate_tempest_plugin/tests/api/v2/test_blacklists.py
+++ b/designate_tempest_plugin/tests/api/v2/test_blacklists.py
@@ -30,8 +30,7 @@
 
 class BlacklistsAdminTest(BaseBlacklistsTest):
 
-    credentials = ["admin", "system_admin"]
-
+    credentials = ["admin", "system_admin", "primary"]
     @classmethod
     def setup_credentials(cls):
         # Do not create network resources for these test.
@@ -41,10 +40,12 @@
     @classmethod
     def setup_clients(cls):
         super(BlacklistsAdminTest, cls).setup_clients()
+
         if CONF.enforce_scope.designate:
             cls.admin_client = cls.os_system_admin.dns_v2.BlacklistsClient()
         else:
             cls.admin_client = cls.os_admin.dns_v2.BlacklistsClient()
+        cls.primary_client = cls.os_primary.dns_v2.BlacklistsClient()
 
     @decorators.idempotent_id('3a7f7564-6bdd-446e-addc-a3475b4c3f71')
     def test_create_blacklist(self):
@@ -58,6 +59,30 @@
 
         self.assertExpected(blacklist, body, self.excluded_keys)
 
+    @decorators.idempotent_id('ea608152-da3c-11eb-b8b8-74e5f9e2a801')
+    @decorators.skip_because(bug="1934252")
+    def test_create_blacklist_invalid_pattern(self):
+        patterns = ['', '#(*&^%$%$#@$', 'a' * 1000]
+        for pattern in patterns:
+            LOG.info(
+                'Try to create a blacklist using pattern:{}'.format(pattern))
+            self.assertRaises(
+                lib_exc.BadRequest, self.admin_client.create_blacklist,
+                pattern=pattern)
+
+    @decorators.idempotent_id('664bdaa0-da47-11eb-b8b8-74e5f9e2a801')
+    def test_create_blacklist_huge_size_description(self):
+        LOG.info('Try to create a blacklist using huge size description')
+        self.assertRaises(
+            lib_exc.BadRequest, self.admin_client.create_blacklist,
+            description='a' * 1000)
+
+    @decorators.idempotent_id('fe9de464-d8d1-11eb-bcdc-74e5f9e2a801')
+    def test_create_blacklist_as_primary_fails(self):
+        LOG.info('As Primary user, try to create a blacklist')
+        self.assertRaises(
+            lib_exc.Forbidden, self.primary_client.create_blacklist)
+
     @decorators.idempotent_id('5bc02942-6225-4619-8f49-2105581a8dd6')
     def test_show_blacklist(self):
         LOG.info('Create a blacklist')
diff --git a/designate_tempest_plugin/tests/api/v2/test_enabled_api_version.py b/designate_tempest_plugin/tests/api/v2/test_enabled_api_version.py
new file mode 100644
index 0000000..db98a9d
--- /dev/null
+++ b/designate_tempest_plugin/tests/api/v2/test_enabled_api_version.py
@@ -0,0 +1,71 @@
+# Copyright 2021 Red Hat.
+#
+# 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.
+import requests
+
+from oslo_log import log as logging
+from tempest.lib import decorators
+
+from designate_tempest_plugin.tests import base
+from designate_tempest_plugin.services.dns.v2.json import base as service_base
+
+from tempest import config
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+class DesignateApiVersion(base.BaseDnsV2Test, service_base.DnsClientV2Base):
+    credentials = ['admin', 'primary']
+
+    @classmethod
+    def setup_credentials(cls):
+        # Do not create network resources for these test.
+        cls.set_network_resources()
+        super(DesignateApiVersion, cls).setup_credentials()
+
+    @classmethod
+    def setup_clients(cls):
+        super(DesignateApiVersion, cls).setup_clients()
+
+        cls.admin_client = cls.os_admin.dns_v2.ApiVersionClient()
+        cls.primary_client = cls.os_primary.dns_v2.ApiVersionClient()
+
+    @decorators.idempotent_id('aa84986e-f2ad-11eb-b58d-74e5f9e2a801')
+    def test_list_enabled_api_versions(self):
+        for user in ['admin', 'primary', 'not_auth_user']:
+            if user == 'admin':
+                versions = self.admin_client.list_enabled_api_versions()[1][
+                    'versions']['values']
+            if user == 'primary':
+                versions = self.primary_client.list_enabled_api_versions()[1][
+                    'versions']['values']
+            if user == 'not_auth_user':
+                uri = CONF.identity.uri.split('identity')[0] + 'dns'
+                response = requests.get(uri, verify=False)
+                headers = {
+                    k.lower(): v.lower() for k, v in response.headers.items()}
+                versions = self.deserialize(
+                    headers, str(response.text))['versions']['values']
+
+            LOG.info('Received enabled API versions for {} '
+                     'user are:{}'.format(user, versions))
+            for item in versions:
+                enabled_ids = [
+                    item['id'] for key in item.keys() if key == 'id']
+            LOG.info('Enabled versions IDs are:{}'.format(enabled_ids))
+            possible_options = [['v1'], ['v2'], ['v1', 'v2']]
+            self.assertIn(
+                enabled_ids, possible_options,
+                'Failed, received version: {} is not in possible options'
+                ' list:{}'.format(enabled_ids, possible_options))
diff --git a/designate_tempest_plugin/tests/api/v2/test_quotas.py b/designate_tempest_plugin/tests/api/v2/test_quotas.py
index 9adf62e..db870af 100644
--- a/designate_tempest_plugin/tests/api/v2/test_quotas.py
+++ b/designate_tempest_plugin/tests/api/v2/test_quotas.py
@@ -15,6 +15,7 @@
 from tempest import config
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
+from tempest.lib.common.utils import data_utils as tempest_data_utils
 
 from designate_tempest_plugin.tests import base
 from designate_tempest_plugin import data_utils as dns_data_utils
@@ -52,6 +53,7 @@
             cls.admin_client = cls.os_admin.dns_v2.QuotasClient()
         cls.quotas_client = cls.os_primary.dns_v2.QuotasClient()
         cls.alt_client = cls.os_alt.dns_v2.QuotasClient()
+        cls.alt_zone_client = cls.os_alt.dns_v2.ZonesClient()
 
     def _store_quotas(self, project_id, cleanup=True):
         """Remember current quotas and reset them after the test"""
@@ -172,11 +174,11 @@
         if not CONF.dns_feature_enabled.api_v2_quotas_verify_project:
             raise self.skipException("Project ID in quotas "
                                      "is not being verified.")
-
+        original_quotas = self.quotas_client.show_quotas(
+            project_id=self.quotas_client.project_id)[1]
         project_id = 'project-that-does-not-exist'
 
         LOG.info("Updating quotas for non-existing %s ", project_id)
-
         quotas = dns_data_utils.rand_quotas()
         request = quotas.copy()
         with self.assertRaisesDns(lib_exc.BadRequest, 'invalid_project', 400):
@@ -184,3 +186,75 @@
                 project_id=project_id,
                 headers=self.all_projects_header,
                 **request)
+
+        LOG.info("Make sure that the quotas weren't changed")
+        _, client_body = self.quotas_client.show_quotas(
+            project_id=self.quotas_client.project_id)
+        self.assertExpected(original_quotas, client_body, [])
+
+    @decorators.idempotent_id('ae82a0ba-da60-11eb-bf12-74e5f9e2a801')
+    def test_admin_sets_quota_for_a_project(self):
+
+        primary_project_id = self.quotas_client.project_id
+        http_headers_to_use = [
+            {'X-Auth-All-Projects': True},
+            {'x-auth-sudo-project-id': primary_project_id}]
+
+        for http_header in http_headers_to_use:
+            LOG.info('As Admin user set Zones quota for a Primary user and {} '
+                     'HTTP header'.format(http_header))
+            quotas = dns_data_utils.rand_quotas()
+            self.admin_client.set_quotas(
+                project_id=primary_project_id,
+                quotas=quotas, headers=http_header)
+            self.addCleanup(self.admin_client.delete_quotas,
+                            project_id=primary_project_id)
+
+            LOG.info("As Admin fetch the quotas for a Primary user")
+            body = self.admin_client.show_quotas(
+                project_id=primary_project_id, headers=http_header)[1]
+            LOG.info('Ensuring that the "set" and "shown" quotas are same')
+            self.assertExpected(quotas, body, [])
+
+    @decorators.idempotent_id('40b9d7ac-da5f-11eb-bf12-74e5f9e2a801')
+    def test_primary_fails_to_set_quota(self):
+
+        primary_project_id = self.quotas_client.project_id
+        LOG.info('Try to set quota as Primary user')
+        self.assertRaises(
+            lib_exc.Forbidden, self.quotas_client.set_quotas,
+            project_id=primary_project_id,
+            quotas=dns_data_utils.rand_quotas())
+
+        LOG.info('Try to set quota as Primary user using '
+                 '"x-auth-sudo-project-id" HTTP header')
+        self.assertRaises(
+            lib_exc.Forbidden, self.quotas_client.set_quotas,
+            project_id=self.quotas_client.project_id,
+            quotas=dns_data_utils.rand_quotas(),
+            headers={'x-auth-sudo-project-id': primary_project_id})
+
+        LOG.info('Try to set quota as Primary user using '
+                 '"x-auth-all-projects" HTTP header')
+        self.assertRaises(
+            lib_exc.Forbidden, self.quotas_client.set_quotas,
+            project_id=self.quotas_client.project_id,
+            quotas=dns_data_utils.rand_quotas(),
+            headers={'x-auth-all-projects': True})
+
+    @decorators.idempotent_id('a6ce5b46-dcce-11eb-903e-74e5f9e2a801')
+    @decorators.skip_because(bug="1934596")
+    def test_admin_sets_invalid_quota_values(self):
+
+        primary_project_id = self.quotas_client.project_id
+        http_header = {'X-Auth-All-Projects': True}
+
+        for item in ['zones', 'zone_records',
+                     'zone_recordsets', 'recordset_records']:
+            quota = dns_data_utils.rand_quotas()
+            quota[item] = tempest_data_utils.rand_name()
+            self.assertRaises(
+                lib_exc.BadRequest, self.admin_client.set_quotas,
+                project_id=primary_project_id,
+                quotas=quota,
+                headers=http_header)
diff --git a/designate_tempest_plugin/tests/api/v2/test_service_statuses.py b/designate_tempest_plugin/tests/api/v2/test_service_statuses.py
index 32db61a..0deeb74 100644
--- a/designate_tempest_plugin/tests/api/v2/test_service_statuses.py
+++ b/designate_tempest_plugin/tests/api/v2/test_service_statuses.py
@@ -15,6 +15,8 @@
 from designate_tempest_plugin.common import constants as const
 from tempest import config
 from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
 
 from designate_tempest_plugin.tests import base
 
@@ -24,7 +26,7 @@
 
 class ServiceStatus(base.BaseDnsV2Test):
 
-    credentials = ["primary", "admin", "system_admin"]
+    credentials = ["primary", "admin", "system_admin", "alt"]
 
     @classmethod
     def setup_credentials(cls):
@@ -41,8 +43,11 @@
             cls.admin_client = cls.os_admin.dns_v2.ServiceClient()
         cls.client = cls.os_primary.dns_v2.ServiceClient()
 
+        cls.primary_client = cls.os_primary.dns_v2.ServiceClient()
+        cls.alt_client = cls.os_alt.dns_v2.ServiceClient()
+
     @decorators.idempotent_id('bf277a76-8583-11eb-a557-74e5f9e2a801')
-    def test_list_service_statuses(self):
+    def test_admin_list_service_statuses(self):
 
         services_statuses_tup = [
             (item['service_name'],
@@ -64,3 +69,19 @@
             {const.UP}, set([item[1] for item in services_statuses_tup]),
             "Failed, not all listed services are in UP status, "
             "services: {}".format(services_statuses_tup))
+
+    @decorators.idempotent_id('d4753f76-de43-11eb-91d1-74e5f9e2a801')
+    def test_primary_is_forbidden_to_list_service_statuses(self):
+
+        LOG.info('Try to "list service statuses" as Primary user')
+        self.assertRaises(
+            lib_exc.Forbidden, self.primary_client.list_statuses)
+
+        headers = [{'x-auth-all-projects': True},
+                   {'x-auth-sudo-project-id': self.alt_client.project_id}]
+        for header in headers:
+            LOG.info('Try to "list service statuses" using {} '
+                     'HTTP header'.format(header))
+            self.assertRaises(
+                lib_exc.Forbidden, self.primary_client.list_statuses,
+                headers=header)
diff --git a/designate_tempest_plugin/tests/api/v2/test_zones.py b/designate_tempest_plugin/tests/api/v2/test_zones.py
index 0462d8b..dec5028 100644
--- a/designate_tempest_plugin/tests/api/v2/test_zones.py
+++ b/designate_tempest_plugin/tests/api/v2/test_zones.py
@@ -52,6 +52,7 @@
         else:
             cls.pool_client = cls.os_admin.dns_v2.PoolClient()
         cls.client = cls.os_primary.dns_v2.ZonesClient()
+        cls.recordset_client = cls.os_primary.dns_v2.RecordsetClient()
 
     @decorators.idempotent_id('9d2e20fc-e56f-4a62-9c61-9752a9ec615c')
     def test_create_zones(self):
@@ -78,6 +79,31 @@
         self.assertEqual('CREATE', zone['action'])
         self.assertEqual('PENDING', zone['status'])
 
+    @decorators.idempotent_id('ec150c22-f52e-11eb-b09b-74e5f9e2a801')
+    def test_create_zone_validate_recordsets_created(self):
+        # Create a PRIMARY zone and wait till it's Active
+        LOG.info('Create a PRIMARY zone')
+        zone = self.client.create_zone()[1]
+        self.addCleanup(self.wait_zone_delete, self.client, zone['id'])
+
+        LOG.info('Ensure we respond with CREATE+PENDING')
+        self.assertEqual('CREATE', zone['action'])
+        self.assertEqual('PENDING', zone['status'])
+
+        LOG.info('Wait till the zone is Active')
+        waiters.wait_for_zone_status(self.client, zone['id'], 'ACTIVE')
+
+        LOG.info('Ensure that SOA and NS recordsets types has been created.')
+        recordsets = self.recordset_client.list_recordset(
+            zone['id'])[1]['recordsets']
+        types = [rec['type'] for rec in recordsets]
+        expected_types = ['SOA', 'NS']
+        for exp_type in expected_types:
+            self.assertIn(
+                exp_type, types,
+                'Failed, expected recordset type:{} was'
+                ' not created'.format(exp_type))
+
     @decorators.idempotent_id('02ca5d6a-86ce-4f02-9d94-9e5db55c3055')
     def test_show_zone(self):
         LOG.info('Create a zone')
diff --git a/designate_tempest_plugin/tests/scenario/v2/test_blacklists.py b/designate_tempest_plugin/tests/scenario/v2/test_blacklists.py
new file mode 100644
index 0000000..1501300
--- /dev/null
+++ b/designate_tempest_plugin/tests/scenario/v2/test_blacklists.py
@@ -0,0 +1,102 @@
+# Copyright 2021 Red Hat.
+#
+# 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.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from designate_tempest_plugin import data_utils as dns_data_utils
+from designate_tempest_plugin.tests import base
+
+LOG = logging.getLogger(__name__)
+
+
+class BaseBlacklistsTest(base.BaseDnsV2Test):
+    excluded_keys = ['created_at', 'updated_at', 'links']
+
+
+class BlacklistE2E(BaseBlacklistsTest):
+
+    credentials = ["admin", 'primary']
+
+    @classmethod
+    def setup_credentials(cls):
+        # Do not create network resources for these test.
+        cls.set_network_resources()
+        super(BlacklistE2E, cls).setup_credentials()
+
+    @classmethod
+    def setup_clients(cls):
+        super(BlacklistE2E, cls).setup_clients()
+        cls.admin_blacklist_client = cls.os_admin.dns_v2.BlacklistsClient()
+        cls.admin_zone_client = cls.os_admin.dns_v2.ZonesClient()
+        cls.primary_zone_client = cls.os_primary.dns_v2.ZonesClient()
+
+    @decorators.idempotent_id('22b1ee72-d8d2-11eb-bcdc-74e5f9e2a801')
+    def test_primary_fails_to_create_zone_matches_blacklist_regex(self):
+        LOG.info('Create a blacklist using regex')
+        blacklist = {
+            'pattern': '^blacklistregextest.*',
+            'description': 'Zone starts with "blacklistregextest" char'}
+        body = self.admin_blacklist_client.create_blacklist(**blacklist)[1]
+        self.addCleanup(
+            self.admin_blacklist_client.delete_blacklist, body['id'])
+
+        LOG.info('Try to create a zone that is starts with '
+                 '"blacklistregextest".')
+        self.assertRaisesDns(
+            lib_exc.BadRequest, 'invalid_zone_name', 400,
+            self.primary_zone_client.create_zone,
+            name='blacklistregextest' + dns_data_utils.rand_zone_name())
+
+    @decorators.idempotent_id('6956f20c-d8d5-11eb-bcdc-74e5f9e2a801')
+    def test_primary_fails_to_create_zone_matches_blacklist_name(self):
+        LOG.info('Create a blacklist using the exact name(string)')
+        zone_name = 'blacklistnametest' + dns_data_utils.rand_zone_name()
+        blacklist = {
+            'pattern': zone_name,
+            'description': 'Zone named:{} '.format(zone_name)}
+        body = self.admin_blacklist_client.create_blacklist(**blacklist)[1]
+        self.addCleanup(
+            self.admin_blacklist_client.delete_blacklist, body['id'])
+
+        LOG.info('Try to create a zone named:{}'.format(zone_name))
+        self.assertRaisesDns(
+            lib_exc.BadRequest, 'invalid_zone_name', 400,
+            self.primary_zone_client.create_zone, name=zone_name)
+
+    @decorators.idempotent_id('de030088-d97e-11eb-8ab8-74e5f9e2a801')
+    def test_admin_creates_zone_matches_blacklist_name_or_regex(self):
+        LOG.info('Create a blacklists using: regex and exact string(name)')
+        zone_name = 'blacklistnameregextest1' + dns_data_utils.rand_zone_name()
+        blacklists = [
+            {'pattern': '^blacklistnameregextest2.*',
+             'description': 'Zone starts with "a" char'},
+            {'pattern': zone_name,
+             'description': 'Deny if Zone named:{} '.format(zone_name)}]
+        for blacklist in blacklists:
+            body = self.admin_blacklist_client.create_blacklist(**blacklist)[1]
+            self.addCleanup(
+                self.admin_blacklist_client.delete_blacklist, body['id'])
+
+        LOG.info('As Admin user try to create zones that are '
+                 'supposed to be blocked')
+        zone = self.admin_zone_client.create_zone(
+            name='blacklistnameregextest2' +
+                 dns_data_utils.rand_zone_name())[1]
+        self.addCleanup(
+            self.wait_zone_delete, self.admin_zone_client, zone['id'])
+        zone = self.admin_zone_client.create_zone(name=zone_name)[1]
+        self.addCleanup(
+            self.wait_zone_delete, self.admin_zone_client, zone['id'])
diff --git a/designate_tempest_plugin/tests/scenario/v2/test_quotas.py b/designate_tempest_plugin/tests/scenario/v2/test_quotas.py
new file mode 100644
index 0000000..0c20b3d
--- /dev/null
+++ b/designate_tempest_plugin/tests/scenario/v2/test_quotas.py
@@ -0,0 +1,92 @@
+# Copyright 2021 Red Hat.
+#
+# 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', 'system_admin', 'alt']
+
+    @classmethod
+    def setup_credentials(cls):
+        # Do not create network resources for these test.
+        cls.set_network_resources()
+        super(QuotasV2Test, cls).setup_credentials()
+
+    @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()
+        if CONF.enforce_scope.designate:
+            cls.admin_client = cls.os_system_admin.dns_v2.QuotasClient()
+        else:
+            cls.admin_client = cls.os_admin.dns_v2.QuotasClient()
+        cls.quotas_client = cls.os_primary.dns_v2.QuotasClient()
+        cls.alt_client = cls.os_alt.dns_v2.QuotasClient()
+        cls.alt_zone_client = cls.os_alt.dns_v2.ZonesClient()
+
+    @decorators.idempotent_id('6987953a-dccf-11eb-903e-74e5f9e2a801')
+    def test_alt_reaches_zones_quota(self):
+
+        alt_project_id = self.alt_client.project_id
+        http_header = {'x-auth-sudo-project-id': alt_project_id}
+        limit_zones_quota = 3
+
+        LOG.info('As Admin user set Zones quota for Alt user '
+                 'to:{} '.format(limit_zones_quota))
+        quotas = dns_data_utils.rand_quotas()
+        quotas['zones'] = limit_zones_quota
+        self.admin_client.set_quotas(
+            project_id=alt_project_id, quotas=quotas, headers=http_header)
+        self.addCleanup(
+            self.admin_client.delete_quotas, project_id=alt_project_id)
+
+        LOG.info('As Alt user try to create zones, up untill'
+                 ' "zones" quota (status code 413) is reached')
+        attempt_number = 0
+        while attempt_number <= limit_zones_quota + 1:
+            attempt_number += 1
+            LOG.info('Attempt No:{} '.format(attempt_number))
+            try:
+                zone = self.alt_zone_client.create_zone()[1]
+                self.addCleanup(
+                    self.wait_zone_delete, self.alt_zone_client, zone['id'])
+            except Exception as err:
+                raised_error = str(err).replace(' ', '')
+                if not "'code':413" and "'type':'over_quota'" in raised_error \
+                        and attempt_number == limit_zones_quota + 1:
+                    raise (
+                        "Failed, expected status code 413 (type:over_quota) "
+                        "was not raised or maybe it has been raised mistakenly"
+                        "(bug) before the quota was actually reached."
+                        " Test failed with: {} ".format(err))