Merge "Add share transfer test."
diff --git a/manila_tempest_tests/common/constants.py b/manila_tempest_tests/common/constants.py
index 3488bc5..416de56 100644
--- a/manila_tempest_tests/common/constants.py
+++ b/manila_tempest_tests/common/constants.py
@@ -108,3 +108,6 @@
 SERVER_STATE_UNMANAGE_STARTING = 'unmanage_starting'
 STATUS_SERVER_MIGRATING = 'server_migrating'
 STATUS_SERVER_MIGRATING_TO = 'server_migrating_to'
+
+# Share transfer
+SHARE_TRANSFER_VERSION = "2.77"
diff --git a/manila_tempest_tests/services/share/v2/json/shares_client.py b/manila_tempest_tests/services/share/v2/json/shares_client.py
index fe3e31c..f4538d7 100644
--- a/manila_tempest_tests/services/share/v2/json/shares_client.py
+++ b/manila_tempest_tests/services/share/v2/json/shares_client.py
@@ -373,6 +373,61 @@
         return rest_client.ResponseBody(resp, body)
 
 ###############
+    def create_share_transfer(self, share_id, name=None,
+                              version=LATEST_MICROVERSION):
+        if name is None:
+            name = data_utils.rand_name("tempest-created-share-transfer")
+        post_body = {
+            "transfer": {
+                "share_id": share_id,
+                "name": name
+            }
+        }
+        body = json.dumps(post_body)
+        resp, body = self.post("share-transfers", body, version=version)
+        self.expected_success(202, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_share_transfer(self, transfer_id, version=LATEST_MICROVERSION):
+        resp, body = self.delete("share-transfers/%s" % transfer_id,
+                                 version=version)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def list_share_transfers(self, detailed=False, params=None,
+                             version=LATEST_MICROVERSION):
+        """Get list of share transfers w/o filters."""
+        uri = 'share-transfers/detail' if detailed else 'share-transfers'
+        uri += '?%s' % parse.urlencode(params) if params else ''
+        resp, body = self.get(uri, version=version)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def get_share_transfer(self, transfer_id, version=LATEST_MICROVERSION):
+        resp, body = self.get("share-transfers/%s" % transfer_id,
+                              version=version)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def accept_share_transfer(self, transfer_id, auth_key,
+                              clear_access_rules=False,
+                              version=LATEST_MICROVERSION):
+        post_body = {
+            "accept": {
+                "auth_key": auth_key,
+                "clear_access_rules": clear_access_rules
+            }
+        }
+        body = json.dumps(post_body)
+        resp, body = self.post("share-transfers/%s/accept" % transfer_id,
+                               body, version=version)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+###############
 
     def get_instances_of_share(self, share_id, version=LATEST_MICROVERSION):
         resp, body = self.get("shares/%s/instances" % share_id,
diff --git a/manila_tempest_tests/tests/api/test_share_transfers.py b/manila_tempest_tests/tests/api/test_share_transfers.py
new file mode 100644
index 0000000..56f8e0c
--- /dev/null
+++ b/manila_tempest_tests/tests/api/test_share_transfers.py
@@ -0,0 +1,107 @@
+# Copyright (C) 2022 China Telecom Digital Intelligence.
+# All Rights Reserved.
+#
+#    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 import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from testtools import testcase as tc
+
+from manila_tempest_tests.common import constants
+from manila_tempest_tests.common import waiters
+from manila_tempest_tests.tests.api import base
+from manila_tempest_tests import utils
+
+CONF = config.CONF
+
+
+class ShareTransferTest(base.BaseSharesMixedTest):
+
+    @classmethod
+    def skip_checks(cls):
+        super(ShareTransferTest, cls).skip_checks()
+        utils.check_skip_if_microversion_not_supported(
+            constants.SHARE_TRANSFER_VERSION)
+        if CONF.share.multitenancy_enabled:
+            raise cls.skipException(
+                'Only for driver_handles_share_servers = False driver mode.')
+
+    @classmethod
+    def resource_setup(cls):
+        super(ShareTransferTest, cls).resource_setup()
+        # create share_type with dhss=False
+        extra_specs = cls.add_extra_specs_to_dict()
+        cls.share_type = cls.create_share_type(extra_specs=extra_specs)
+        cls.share_type_id = cls.share_type['id']
+
+    @decorators.idempotent_id('716e71a0-8265-4410-9170-08714095d9e8')
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    def test_create_and_delete_share_transfer(self):
+        # create share
+        share_name = data_utils.rand_name("tempest-share-name")
+        share = self.create_share(name=share_name,
+                                  share_type_id=self.share_type_id,
+                                  cleanup_in_class=False)
+
+        # create share transfer
+        transfer = self.shares_v2_client.create_share_transfer(
+            share['id'], name='tempest_share_transfer')['transfer']
+        waiters.wait_for_resource_status(
+            self.shares_client, share['id'], 'awaiting_transfer')
+
+        # check transfer exists and show transfer
+        transfer_show = self.shares_v2_client.get_share_transfer(
+            transfer['id'])['transfer']
+        self.assertEqual(transfer_show['name'], 'tempest_share_transfer')
+
+        # delete share transfer
+        self.shares_v2_client.delete_share_transfer(transfer['id'])
+        waiters.wait_for_resource_status(
+            self.shares_client, share['id'], 'available')
+
+        # check transfer not in transfer list
+        transfers = self.shares_v2_client.list_share_transfers()['transfers']
+        transfer_ids = [tf['id'] for tf in transfers]
+        self.assertNotIn(transfer['id'], transfer_ids)
+
+    @decorators.idempotent_id('3c2622ab-3368-4693-afb6-e60bd27e61ef')
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    def test_create_and_accept_share_transfer(self):
+        # create share
+        share_name = data_utils.rand_name("tempest-share-name")
+        share = self.create_share(name=share_name,
+                                  share_type_id=self.share_type_id)
+
+        # create share transfer
+        transfer = self.shares_v2_client.create_share_transfer(
+            share['id'])['transfer']
+        waiters.wait_for_resource_status(
+            self.shares_client, share['id'], 'awaiting_transfer')
+
+        # accept share transfer by alt project
+        self.alt_shares_v2_client.accept_share_transfer(transfer['id'],
+                                                        transfer['auth_key'])
+        waiters.wait_for_resource_status(
+            self.alt_shares_client, share['id'], 'available')
+
+        # check share in alt project
+        shares = self.alt_shares_v2_client.list_shares(
+            detailed=True)['shares']
+        share_ids = [sh['id'] for sh in shares] if shares else []
+        self.assertIn(share['id'], share_ids)
+
+        # delete the share
+        self.alt_shares_v2_client.delete_share(share['id'])
+        self.alt_shares_v2_client.wait_for_resource_deletion(
+            share_id=share["id"])
diff --git a/manila_tempest_tests/tests/api/test_share_transfers_negative.py b/manila_tempest_tests/tests/api/test_share_transfers_negative.py
new file mode 100644
index 0000000..0672459
--- /dev/null
+++ b/manila_tempest_tests/tests/api/test_share_transfers_negative.py
@@ -0,0 +1,137 @@
+# Copyright (C) 2022 China Telecom Digital Intelligence.
+# All Rights Reserved.
+#
+#    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_utils import uuidutils
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+from testtools import testcase as tc
+
+from manila_tempest_tests.common import constants
+from manila_tempest_tests.common import waiters
+from manila_tempest_tests.tests.api import base
+from manila_tempest_tests import utils
+
+CONF = config.CONF
+
+
+class ShareTransferNegativeTest(base.BaseSharesMixedTest):
+
+    @classmethod
+    def skip_checks(cls):
+        super(ShareTransferNegativeTest, cls).skip_checks()
+        utils.check_skip_if_microversion_not_supported(
+            constants.SHARE_TRANSFER_VERSION)
+        if CONF.share.multitenancy_enabled:
+            raise cls.skipException(
+                'Only for driver_handles_share_servers = False driver mode.')
+
+    @classmethod
+    def resource_setup(cls):
+        super(ShareTransferNegativeTest, cls).resource_setup()
+        # create share_type with dhss=False
+        extra_specs = cls.add_extra_specs_to_dict()
+        cls.share_type = cls.create_share_type(extra_specs=extra_specs)
+        cls.share_type_id = cls.share_type['id']
+
+    def _create_share_transfer(self, share):
+        transfer = self.shares_v2_client.create_share_transfer(
+            share['id'])['transfer']
+        waiters.wait_for_resource_status(
+            self.shares_client, share['id'], 'awaiting_transfer')
+        self.addCleanup(waiters.wait_for_resource_status, self.shares_client,
+                        share['id'], 'available')
+        self.addCleanup(self.shares_v2_client.delete_share_transfer,
+                        transfer['id'])
+        return transfer
+
+    @decorators.idempotent_id('baf66f62-253e-40dd-a6a9-109bc7613e52')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    def test_show_transfer_of_other_tenants(self):
+        # create share
+        share_name = data_utils.rand_name("tempest-share-name")
+        share = self.create_share(
+            name=share_name,
+            share_type_id=self.share_type_id)
+
+        # create share transfer
+        transfer = self._create_share_transfer(share)
+
+        self.assertRaises(lib_exc.NotFound,
+                          self.alt_shares_v2_client.get_share_transfer,
+                          transfer['id'])
+
+    @decorators.idempotent_id('4b9e75b1-4ac6-4111-b09e-e6dacd0ac2c3')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
+    def test_show_nonexistent_transfer(self):
+        self.assertRaises(lib_exc.NotFound,
+                          self.shares_v2_client.get_share_transfer,
+                          str(uuidutils.generate_uuid()))
+
+    @decorators.idempotent_id('b3e26356-5eb0-4f73-b5a7-d3594cc2f30e')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    def test_delete_transfer_of_other_tenants(self):
+        # create share
+        share_name = data_utils.rand_name("tempest-share-name")
+        share = self.create_share(
+            name=share_name,
+            share_type_id=self.share_type_id)
+
+        # create share transfer
+        transfer = self._create_share_transfer(share)
+
+        self.assertRaises(lib_exc.NotFound,
+                          self.alt_shares_v2_client.delete_share_transfer,
+                          transfer['id'])
+
+    @decorators.idempotent_id('085d5971-fe6e-4497-93cb-f1eb176a10da')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
+    def test_delete_nonexistent_transfer(self):
+        self.assertRaises(lib_exc.NotFound,
+                          self.shares_v2_client.delete_share_transfer,
+                          str(uuidutils.generate_uuid()))
+
+    @decorators.idempotent_id('cc7af032-0504-417e-8ab9-73b37bed7f85')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    def test_accept_transfer_without_auth_key(self):
+        # create share
+        share_name = data_utils.rand_name("tempest-share-name")
+        share = self.create_share(
+            name=share_name,
+            share_type_id=self.share_type_id)
+
+        # create share transfer
+        transfer = self._create_share_transfer(share)
+
+        self.assertRaises(lib_exc.BadRequest,
+                          self.alt_shares_v2_client.accept_share_transfer,
+                          transfer['id'], "")
+
+    @decorators.idempotent_id('05a6a345-7609-421f-be21-d79041970674')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
+    def test_accept_transfer_with_incorrect_auth_key(self):
+        # create share
+        share_name = data_utils.rand_name("tempest-share-name")
+        share = self.create_share(
+            name=share_name,
+            share_type_id=self.share_type_id)
+
+        # create share transfer
+        transfer = self._create_share_transfer(share)
+
+        self.assertRaises(lib_exc.BadRequest,
+                          self.alt_shares_v2_client.accept_share_transfer,
+                          transfer['id'], "incorrect_auth_key")