Test cases for: "FloatingIPs PTR" resords.

1) test_set_floatingip_ptr
   Set a PTR record for the given FloatingIP.
2) test_show_floatingip_ptr
   Shows a particular FloatingIP PTR
3) test_list_floatingip_ptr_records
   List FloatingIP PTR records
4) test_unset_floatingip_ptr
   Unset the PTR record for a FloatingIP
5) test_set_floatingip_ptr_invalid_ttl
   Try to use invalid TTL to set PTR record.
   Expected 400 BadRequest

Change-Id: Ib7a3b829129f2534f67c66c5f58e7c7c3c2f93c9
diff --git a/designate_tempest_plugin/clients.py b/designate_tempest_plugin/clients.py
index 6fbfc05..deafecb 100644
--- a/designate_tempest_plugin/clients.py
+++ b/designate_tempest_plugin/clients.py
@@ -51,6 +51,7 @@
     import SevriceClient
 from designate_tempest_plugin.services.dns.v2.json.designate_limit_client \
     import DesignateLimitClient
+from designate_tempest_plugin.services.dns.v2.json.ptr_client import PtrClient
 
 CONF = config.CONF
 
@@ -99,6 +100,7 @@
         self.tsigkey_client = TsigkeyClient(**params)
         self.service_client = SevriceClient(**params)
         self.designate_limit_client = DesignateLimitClient(**params)
+        self.ptr_client = PtrClient(**params)
 
         self.query_client = QueryClient(
             nameservers=CONF.dns.nameservers,
diff --git a/designate_tempest_plugin/services/dns/json/base.py b/designate_tempest_plugin/services/dns/json/base.py
index 1f12642..cbfb34f 100644
--- a/designate_tempest_plugin/services/dns/json/base.py
+++ b/designate_tempest_plugin/services/dns/json/base.py
@@ -87,17 +87,25 @@
             expected_code=expected_code, read_code=int(read_code),
         )
 
-    def get_uri(self, resource_name, uuid=None, params=None):
+    def get_uri(self, resource_name, uuid=None, params=None,
+                uuid_prefix_char=None):
         """Get URI for a specific resource or object.
         :param resource_name: 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 uuid_prefix_char: applies to override hardcoded ('/')
+                prefix UUID character. This parameter enables to set required
+                by API character, for example ":" instead of "/".
         :returns: Relative URI for the resource or object.
         """
         uri_pattern = '{pref}/{res}{uuid}{params}'
 
-        uuid = '/%s' % uuid if uuid else ''
+        if uuid_prefix_char:
+            uuid = uuid_prefix_char + '%s' % uuid if uuid else ''
+        else:
+            uuid = '/%s' % uuid if uuid else ''
+
         params = '?%s' % urllib.urlencode(params) if params else ''
 
         return uri_pattern.format(pref=self.uri_prefix,
@@ -141,7 +149,7 @@
         return resp, self.deserialize(resp, body)
 
     def _show_request(self, resource, uuid, headers=None, params=None,
-                      extra_headers=False):
+                      extra_headers=False, uuid_prefix_char=None):
         """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.
@@ -152,9 +160,13 @@
                                      method are to be used but additional
                                      headers are needed in the request
                                      pass them in as a dict.
+        :param uuid_prefix_char: applies to override hardcoded ('/')
+                prefix UUID character. This parameter enables to set required
+                by API character, for example ":" instead of "/".
         :returns: Serialized object as a dictionary.
         """
-        uri = self.get_uri(resource, uuid=uuid, params=params)
+        uri = self.get_uri(resource, uuid=uuid, params=params,
+                           uuid_prefix_char=uuid_prefix_char)
 
         resp, body = self.get(
             uri, headers=headers, extra_headers=extra_headers)
@@ -199,7 +211,7 @@
         return resp, self.deserialize(resp, body)
 
     def _update_request(self, resource, uuid, data, params=None, headers=None,
-                        extra_headers=False):
+                        extra_headers=False, uuid_prefix_char=None):
         """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.
@@ -214,13 +226,18 @@
                                      method are to be used but additional
                                      headers are needed in the request
                                      pass them in as a dict.
+        :param uuid_prefix_char: applies to override hardcoded ('/')
+                prefix UUID character. This parameter enables to set required
+                by API character, for example ":" instead of "/".
         :returns: Serialized object as a dictionary.
         """
         body = self.serialize(data)
-        uri = self.get_uri(resource, uuid=uuid, params=params)
+        uri = self.get_uri(
+            resource, uuid=uuid, params=params,
+            uuid_prefix_char=uuid_prefix_char)
 
         resp, body = self.patch(uri, body=body,
-                                headers=headers, extra_headers=True)
+                                headers=headers, extra_headers=extra_headers)
 
         self.expected_success(self.UPDATE_STATUS_CODES, resp.status)
 
diff --git a/designate_tempest_plugin/services/dns/v2/json/ptr_client.py b/designate_tempest_plugin/services/dns/v2/json/ptr_client.py
new file mode 100644
index 0000000..1bd59b3
--- /dev/null
+++ b/designate_tempest_plugin/services/dns/v2/json/ptr_client.py
@@ -0,0 +1,85 @@
+# 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 tempest.lib.common.utils import data_utils
+
+from designate_tempest_plugin import data_utils as dns_data_utils
+from designate_tempest_plugin.services.dns.v2.json import base
+from tempest import config
+
+CONF = config.CONF
+
+
+class PtrClient(base.DnsClientV2Base):
+
+    @base.handle_errors
+    def set_ptr_record(self, floatingip_id, ptr_name=None,
+                       ttl=None, description=None, headers=None):
+        """Set a PTR record for the given FloatingIP
+
+        :param floatingip_id: valid UUID of floating IP to be used.
+        :param ptr_name PTR record name or random if not provided.
+        :param ttl TTL or random valid value if not provided.
+        :param description Description or random if not provided.
+        :param headers (dict): The headers to use for the request.
+        :return: created PTR dictionary.
+        """
+        ptr = {
+            'ptrdname': ptr_name or dns_data_utils.rand_zone_name(),
+            'ttl': ttl or dns_data_utils.rand_ttl(),
+            'description': description or data_utils.rand_name(
+                'test-ptr')}
+
+        return self._update_request(
+            resource='reverse/floatingips/{}'.format(CONF.identity.region),
+            uuid=floatingip_id, data=ptr, headers=headers,
+            uuid_prefix_char=':')[1]
+
+    @base.handle_errors
+    def show_ptr_record(self, floatingip_id, headers=None):
+        """Show PTR record for the given FloatingIP
+
+        :param floatingip_id: valid UUID of floating IP to show.
+        :param headers (dict): The headers to use for the request.
+        :return: Shown PTR dictionary.
+        """
+        return self._show_request(
+            resource='reverse/floatingips/{}'.format(CONF.identity.region),
+            uuid=floatingip_id, headers=headers, uuid_prefix_char=':')[1]
+
+    @base.handle_errors
+    def list_ptr_records(self, headers=None):
+        """List PTR records for the given FloatingIP
+
+        :param headers (dict): The headers to use for the request.
+        :return: List of PTR records.
+        """
+        return self._list_request(
+            'reverse/floatingips', headers=headers)[1]['floatingips']
+
+    @base.handle_errors
+    def unset_ptr_record(self, floatingip_id, headers=None):
+        """Unset the PTR record for a given FloatingIP
+
+        :param floatingip_id: valid UUID of floating IP to unset.
+        :param headers (dict): The headers to use for the request.
+        :return: Tuple (Response, Body)
+        """
+        data = {"ptrdname": None}
+        resp, body = self._update_request(
+            resource='reverse/floatingips/{}'.format(CONF.identity.region),
+            uuid=floatingip_id, data=data, headers=headers,
+            uuid_prefix_char=':')
+        # Unset PTR should Return a HTTP 202
+        self.expected_success(202, resp.status)
+        return resp, body
diff --git a/designate_tempest_plugin/tests/api/v2/test_ptrs.py b/designate_tempest_plugin/tests/api/v2/test_ptrs.py
new file mode 100644
index 0000000..e46f3f8
--- /dev/null
+++ b/designate_tempest_plugin/tests/api/v2/test_ptrs.py
@@ -0,0 +1,118 @@
+# 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 tempest.lib import exceptions as lib_exc
+
+from designate_tempest_plugin.tests import base
+import tempest.test
+
+LOG = logging.getLogger(__name__)
+
+CONF = config.CONF
+
+
+class BasePtrTest(base.BaseDnsV2Test):
+    excluded_keys = ['created_at', 'updated_at', 'version', 'links',
+                    'status', 'action']
+
+
+class DesignatePtrRecord(BasePtrTest, tempest.test.BaseTestCase):
+    credentials = ['primary']
+
+    @classmethod
+    def setup_credentials(cls):
+        # Do not create network resources for these test.
+        cls.set_network_resources()
+        super(DesignatePtrRecord, cls).setup_credentials()
+
+    @classmethod
+    def setup_clients(cls):
+        super(DesignatePtrRecord, cls).setup_clients()
+        cls.primary_ptr_client = cls.os_primary.ptr_client
+        cls.primary_floating_ip_client = cls.os_primary.floating_ips_client
+
+    def _set_ptr(self):
+        fip_id = self.primary_floating_ip_client.create_floatingip(
+            floating_network_id=CONF.network.public_network_id)[
+            'floatingip']['id']
+        ptr = self.primary_ptr_client.set_ptr_record(fip_id)
+        self.assertEqual('CREATE', ptr['action'])
+        self.assertEqual('PENDING', ptr['status'])
+        return fip_id, ptr
+
+    @decorators.idempotent_id('2fb9d6ea-871d-11eb-9f9a-74e5f9e2a801')
+    def test_set_floatingip_ptr(self):
+        self._set_ptr()
+
+    @decorators.idempotent_id('9179325a-87d0-11eb-9f9a-74e5f9e2a801')
+    def test_show_floatingip_ptr(self):
+        fip_id, ptr = self._set_ptr()
+        show_ptr = self.primary_ptr_client.show_ptr_record(
+            floatingip_id=fip_id)
+        self.assertExpected(ptr, show_ptr, self.excluded_keys)
+
+    @decorators.idempotent_id('9187a9c6-87d4-11eb-9f9a-74e5f9e2a801')
+    def test_list_floatingip_ptr_records(self):
+        number_of_ptr_records = 3
+        created_ptr_ids = []
+        for _ in range(number_of_ptr_records):
+            fip_id, ptr = self._set_ptr()
+            created_ptr_ids.append(ptr['id'])
+        received_ptr_ids = sorted(
+            [item['id'] for item in
+             self.primary_ptr_client.list_ptr_records()])
+        self.assertEqual(
+            sorted(created_ptr_ids), received_ptr_ids,
+            'Failed - received PTR IDs: {} are not as'
+            ' expected: {}'.format(created_ptr_ids, received_ptr_ids))
+
+    @decorators.idempotent_id('499b5a7e-87e1-11eb-b412-74e5f9e2a801')
+    def test_unset_floatingip_ptr(self):
+        fip_id, ptr = self._set_ptr()
+        self.primary_ptr_client.unset_ptr_record(fip_id)
+
+
+class DesignatePtrRecordNegative(BasePtrTest, tempest.test.BaseTestCase):
+    credentials = ['primary']
+
+    @classmethod
+    def setup_credentials(cls):
+        # Do not create network resources for these test.
+        cls.set_network_resources()
+        super(DesignatePtrRecordNegative, cls).setup_credentials()
+
+    @classmethod
+    def setup_clients(cls):
+        super(DesignatePtrRecordNegative, cls).setup_clients()
+        cls.primary_ptr_client = cls.os_primary.ptr_client
+        cls.primary_floating_ip_client = cls.os_primary.floating_ips_client
+
+    def _set_ptr(self, ptr_name=None, ttl=None, description=None,
+                 headers=None):
+        fip_id = self.primary_floating_ip_client.create_floatingip(
+            floating_network_id=CONF.network.public_network_id)[
+            'floatingip']['id']
+        ptr = self.primary_ptr_client.set_ptr_record(
+            fip_id, ptr_name=ptr_name, ttl=ttl, description=description,
+            headers=headers)
+        self.assertEqual('CREATE', ptr['action'])
+        self.assertEqual('PENDING', ptr['status'])
+        return fip_id, ptr
+
+    def test_set_floatingip_ptr_invalid_ttl(self):
+        LOG.info('Try to set PTR record using invalid TTL value')
+        with self.assertRaisesDns(lib_exc.BadRequest, 'invalid_object', 400):
+            self._set_ptr(ttl=-10)