Merge "API2.69,Add tests for manila recycle bin."
diff --git a/manila_tempest_tests/common/waiters.py b/manila_tempest_tests/common/waiters.py
index 056dab6..24b30de 100644
--- a/manila_tempest_tests/common/waiters.py
+++ b/manila_tempest_tests/common/waiters.py
@@ -179,3 +179,41 @@
                        ' the required time (%s s).' %
                        (resource_id, client.build_timeout))
             raise exceptions.TimeoutException(message)
+
+
+def wait_for_soft_delete(client, share_id, version=LATEST_MICROVERSION):
+    """Wait for a share soft delete to recycle bin."""
+    share = client.get_share(share_id, version=version)['share']
+    start = int(time.time())
+    while not share['is_soft_deleted']:
+        time.sleep(client.build_interval)
+        share = client.get_share(share_id, version=version)['share']
+        if share['is_soft_deleted']:
+            break
+        elif int(time.time()) - start >= client.build_timeout:
+            message = ('Share %(share_id)s failed to be soft deleted to '
+                       'recycle bin within the required time '
+                       '%(timeout)s.' % {
+                           'share_id': share['id'],
+                           'timeout': client.build_timeout,
+                       })
+            raise exceptions.TimeoutException(message)
+
+
+def wait_for_restore(client, share_id, version=LATEST_MICROVERSION):
+    """Wait for a share restore from recycle bin."""
+    share = client.get_share(share_id, version=version)['share']
+    start = int(time.time())
+    while share['is_soft_deleted']:
+        time.sleep(client.build_interval)
+        share = client.get_share(share_id, version=version)['share']
+        if not share['is_soft_deleted']:
+            break
+        elif int(time.time()) - start >= client.build_timeout:
+            message = ('Share %(share_id)s failed to restore from '
+                       'recycle bin within the required time '
+                       '%(timeout)s.' % {
+                           'share_id': share['id'],
+                           'timeout': client.build_timeout,
+                       })
+            raise exceptions.TimeoutException(message)
diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py
index 92cf7ba..f5022af 100644
--- a/manila_tempest_tests/config.py
+++ b/manila_tempest_tests/config.py
@@ -40,7 +40,7 @@
                     "This value is only used to validate the versions "
                     "response from Manila."),
     cfg.StrOpt("max_api_microversion",
-               default="2.65",
+               default="2.69",
                help="The maximum api microversion is configured to be the "
                     "value of the latest microversion supported by Manila."),
     cfg.StrOpt("region",
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 293c24a..3837049 100644
--- a/manila_tempest_tests/services/share/v2/json/shares_client.py
+++ b/manila_tempest_tests/services/share/v2/json/shares_client.py
@@ -299,6 +299,20 @@
         body = json.loads(body)
         return rest_client.ResponseBody(resp, body)
 
+    def list_shares_in_recycle_bin(self, detailed=False,
+                                   params=None, version=LATEST_MICROVERSION,
+                                   experimental=False):
+        """Get list of shares in recycle bin with w/o filters."""
+        headers = EXPERIMENTAL if experimental else None
+        uri = 'shares/detail' if detailed else 'shares'
+        uri += '?is_soft_deleted=true'
+        uri += '&%s' % parse.urlencode(params) if params else ''
+        resp, body = self.get(uri, headers=headers, extra_headers=experimental,
+                              version=version)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
     def list_shares_with_detail(self, params=None,
                                 version=LATEST_MICROVERSION,
                                 experimental=False):
@@ -342,6 +356,22 @@
         self.expected_success(202, resp.status)
         return rest_client.ResponseBody(resp, body)
 
+    def soft_delete_share(self, share_id, version=LATEST_MICROVERSION):
+        post_body = {"soft_delete": None}
+        body = json.dumps(post_body)
+        resp, body = self.post(
+            "shares/%s/action" % share_id, body, version=version)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def restore_share(self, share_id, version=LATEST_MICROVERSION):
+        post_body = {"restore": None}
+        body = json.dumps(post_body)
+        resp, body = self.post(
+            "shares/%s/action" % share_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):
diff --git a/manila_tempest_tests/tests/api/test_shares_actions.py b/manila_tempest_tests/tests/api/test_shares_actions.py
index 3232d50..779bfb6 100644
--- a/manila_tempest_tests/tests/api/test_shares_actions.py
+++ b/manila_tempest_tests/tests/api/test_shares_actions.py
@@ -687,6 +687,51 @@
         )
         self.assertEqual(new_size, share_get['size'], msg)
 
+    @utils.skip_if_microversion_not_supported("2.69")
+    @decorators.idempotent_id('7a19fb58-b645-44cc-a6d7-b3508ff8754d')
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    def test_soft_delete_and_restore_share(self):
+        share = self.create_share(share_type_id=self.share_type_id)
+
+        # list shares
+        shares = self.shares_v2_client.list_shares()['shares']
+
+        # check the share in share list
+        share_ids = [sh['id'] for sh in shares]
+        self.assertIn(share['id'], share_ids)
+
+        # soft delete the share
+        self.shares_v2_client.soft_delete_share(share['id'])
+        waiters.wait_for_soft_delete(self.shares_v2_client, share['id'])
+
+        # list shares again
+        shares1 = self.shares_v2_client.list_shares()['shares']
+        share_ids1 = [sh['id'] for sh in shares1]
+
+        # list shares in recycle bin
+        shares2 = self.shares_v2_client.list_shares_in_recycle_bin()['shares']
+        share_ids2 = [sh['id'] for sh in shares2]
+
+        # check share has been soft delete to recycle bin
+        self.assertNotIn(share['id'], share_ids1)
+        self.assertIn(share['id'], share_ids2)
+
+        # restore share from recycle bin
+        self.shares_v2_client.restore_share(share['id'])
+        waiters.wait_for_restore(self.shares_v2_client, share['id'])
+
+        # list shares again
+        shares3 = self.shares_v2_client.list_shares()['shares']
+        share_ids3 = [sh['id'] for sh in shares3]
+
+        # list shares in recycle bin again
+        shares4 = self.shares_v2_client.list_shares_in_recycle_bin()['shares']
+        share_ids4 = [sh['id'] for sh in shares4]
+
+        # check share has restored from recycle bin
+        self.assertNotIn(share['id'], share_ids4)
+        self.assertIn(share['id'], share_ids3)
+
 
 class SharesRenameTest(base.BaseSharesMixedTest):
 
diff --git a/manila_tempest_tests/tests/api/test_shares_actions_negative.py b/manila_tempest_tests/tests/api/test_shares_actions_negative.py
index 64818fd..715668c 100644
--- a/manila_tempest_tests/tests/api/test_shares_actions_negative.py
+++ b/manila_tempest_tests/tests/api/test_shares_actions_negative.py
@@ -21,6 +21,7 @@
 import testtools
 from testtools import testcase as tc
 
+from manila_tempest_tests.common import waiters
 from manila_tempest_tests.tests.api import base
 from manila_tempest_tests import utils
 
@@ -294,3 +295,68 @@
         self.assertRaises(lib_exc.NotFound,
                           self.alt_shares_v2_client.get_share,
                           self.share['id'])
+
+    @utils.skip_if_microversion_not_supported("2.69")
+    @decorators.idempotent_id('36cbe23b-08d2-49d9-bb42-f9eb2a804cb1')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    def test_soft_delete_share_has_been_soft_deleted(self):
+        share = self.create_share(share_type_id=self.share_type_id,
+                                  cleanup_in_class=False)
+
+        # soft delete the share
+        self.shares_v2_client.soft_delete_share(share['id'])
+
+        # try soft delete the share again
+        self.assertRaises(lib_exc.Forbidden,
+                          self.shares_v2_client.soft_delete_share,
+                          share['id'])
+
+        # restore the share for resource_cleanup
+        self.shares_v2_client.restore_share(share['id'])
+        waiters.wait_for_restore(self.shares_v2_client, share['id'])
+
+    @utils.skip_if_microversion_not_supported("2.69")
+    @decorators.idempotent_id('cf675ac9-0970-49fc-a051-8a94555c73b5')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    def test_soft_delete_share_with_invalid_share_state(self):
+        share = self.create_share(share_type_id=self.share_type_id,
+                                  cleanup_in_class=False)
+
+        # set "error_deleting" state
+        self.admin_client.reset_state(share['id'], status="error_deleting")
+
+        # try soft delete the share
+        self.assertRaises(lib_exc.Forbidden,
+                          self.shares_v2_client.soft_delete_share,
+                          share['id'])
+
+        # rollback to available status
+        self.admin_client.reset_state(share['id'], status="available")
+
+    @utils.skip_if_microversion_not_supported("2.69")
+    @decorators.idempotent_id('f6106ee4-1a01-444f-b623-912a5e751d49')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    def test_soft_delete_share_from_other_project(self):
+        share = self.create_share(share_type_id=self.share_type_id,
+                                  cleanup_in_class=False)
+
+        # try soft delete the share
+        self.assertRaises(lib_exc.Forbidden,
+                          self.alt_shares_v2_client.soft_delete_share,
+                          share['id'])
+
+    @utils.skip_if_microversion_not_supported("2.69")
+    @decorators.idempotent_id('0ccd44dd-2fda-403e-bc23-7ce428550f36')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
+    def test_soft_delete_share_with_wrong_id(self):
+        self.assertRaises(lib_exc.NotFound,
+                          self.alt_shares_v2_client.soft_delete_share,
+                          "wrong_share_id")
+
+    @utils.skip_if_microversion_not_supported("2.69")
+    @decorators.idempotent_id('87345725-f187-4d7d-86b1-62284e8c75ae')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
+    def test_restore_share_with_wrong_id(self):
+        self.assertRaises(lib_exc.NotFound,
+                          self.alt_shares_v2_client.restore_share,
+                          "wrong_share_id")