Add tests for encryption_key_ref

Positive test -
 1. create barbican secret and create share using secret UUID

Negative test -
 1. invalid encryption key ref
 2. invalid share type extra-spec
 3. absent encryption key ref

partially-implements: blueprint share-encryption
Depends-On: https://review.opendev.org/c/openstack/requirements/+/963685
Change-Id: I3145f9cd6847464b2920f1b0a35e6c211e45b26e
Signed-off-by: Kiran Pawar <kinpaa@gmail.com>
diff --git a/manila_tempest_tests/common/barbican_client_mgr.py b/manila_tempest_tests/common/barbican_client_mgr.py
new file mode 100644
index 0000000..ee8b53f
--- /dev/null
+++ b/manila_tempest_tests/common/barbican_client_mgr.py
@@ -0,0 +1,73 @@
+# Copyright 2025 Cloudifcation GmbH.  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.
+
+import base64
+import secrets
+
+from oslo_log import log as logging
+from tempest import config
+from tempest.lib.services import clients
+from tempest import test
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+class BarbicanClientManager(test.BaseTestCase):
+    """Class for interacting with the barbican service.
+
+    This class is an abstraction for interacting with the barbican service.
+    """
+
+    credentials = ['primary',]
+
+    @classmethod
+    def setup_clients(cls, tempest_client_mgr):
+        super(BarbicanClientManager, cls).setup_clients()
+        if CONF.identity.auth_version == 'v3':
+            auth_uri = CONF.identity.uri_v3
+        else:
+            auth_uri = CONF.identity.uri
+        service_clients = clients.ServiceClients(
+            tempest_client_mgr.credentials,
+            auth_uri)
+        cls.secret_client = service_clients.secret_v1.SecretClient(
+            service='key-manager')
+
+    @classmethod
+    def ref_to_uuid(cls, href):
+        return href.split('/')[-1]
+
+    def store_secret(self):
+        """Store a secret in barbican.
+
+        :returns: The barbican secret_ref.
+        """
+
+        key = secrets.token_bytes(32)
+
+        manila_secret = self.secret_client.create_secret(
+            algorithm='AES',
+            bit_length=256,
+            secret_type='symmetric',
+            payload=base64.b64encode(key).decode(),
+            payload_content_type='application/octet-stream',
+            payload_content_encoding='base64',
+            mode='CBC'
+        )
+        LOG.debug('Manila Secret has ref %s', manila_secret.get('secret_ref'))
+        return manila_secret.get('secret_ref')
+
+    def delete_secret(self, secret_ref):
+        self.secret_client.delete_secret(secret_ref)
diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py
index 60d4164..5539bb5 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.88",
+               default="2.90",
                help="The maximum api microversion is configured to be the "
                     "value of the latest microversion supported by Manila."),
     cfg.StrOpt("region",
@@ -220,6 +220,11 @@
                       "ss_type:<ldap, kerberos or active_directory>, "
                       "ss_dns_ip:value, ss_user:value, ss_password=value, "
                       "ss_domain:value, ss_server:value"),
+    cfg.ListOpt("capability_encryption_support",
+                default=[],
+                help="Encryption support capability. Possible values are "
+                     "share_server, share etc. "),
+
 
     # Switching ON/OFF test suites filtered by features
     cfg.BoolOpt("run_quota_tests",
@@ -386,4 +391,7 @@
     cfg.DictOpt("driver_assisted_backup_test_driver_options",
                 default={'dummy': True},
                 help="Share backup driver options specified as dict."),
+    cfg.BoolOpt("run_encryption_tests",
+                default=False,
+                help="Enable or disable share encryption tests."),
 ]
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 4b35687..7457fff 100644
--- a/manila_tempest_tests/services/share/v2/json/shares_client.py
+++ b/manila_tempest_tests/services/share/v2/json/shares_client.py
@@ -248,7 +248,7 @@
                      share_type_id=None, is_public=False,
                      share_group_id=None, availability_zone=None,
                      version=LATEST_MICROVERSION, experimental=False,
-                     scheduler_hints=None):
+                     scheduler_hints=None, encryption_key_ref=None):
         headers = EXPERIMENTAL if experimental else None
         metadata = metadata or {}
         scheduler_hints = scheduler_hints or {}
@@ -283,6 +283,8 @@
             post_body["share"]["share_group_id"] = share_group_id
         if scheduler_hints:
             post_body["share"]["scheduler_hints"] = scheduler_hints
+        if encryption_key_ref:
+            post_body["share"]["encryption_key_ref"] = encryption_key_ref
 
         body = json.dumps(post_body)
         resp, body = self.post("shares", body, headers=headers,
@@ -1102,7 +1104,7 @@
                       share_networks=None,
                       share_groups=None, share_group_snapshots=None,
                       force=True, share_type=None, share_replicas=None,
-                      replica_gigabytes=None, url=None,
+                      replica_gigabytes=None, encryption_keys=None, url=None,
                       version=LATEST_MICROVERSION):
         if url is None:
             url = self._get_quotas_url(version)
@@ -1130,6 +1132,8 @@
             put_body["share_replicas"] = share_replicas
         if replica_gigabytes is not None:
             put_body["replica_gigabytes"] = replica_gigabytes
+        if encryption_keys is not None:
+            put_body["encryption_keys"] = encryption_keys
         put_body = json.dumps({"quota_set": put_body})
 
         resp, body = self.put(url, put_body, version=version)
diff --git a/manila_tempest_tests/tests/api/admin/test_quotas.py b/manila_tempest_tests/tests/api/admin/test_quotas.py
index e8377e1..74c4622 100644
--- a/manila_tempest_tests/tests/api/admin/test_quotas.py
+++ b/manila_tempest_tests/tests/api/admin/test_quotas.py
@@ -66,6 +66,8 @@
         if utils.share_replica_quotas_are_supported():
             self.assertGreater(int(quotas["share_replicas"]), -2)
             self.assertGreater(int(quotas["replica_gigabytes"]), -2)
+        if utils.encryption_keys_quota_supported():
+            self.assertGreater(int(quotas["encryption_keys"]), -2)
 
     @decorators.idempotent_id('1ff57cfa-cd8d-495f-86eb-9fead307428e')
     @tc.attr(base.TAG_POSITIVE, base.TAG_API)
@@ -82,6 +84,8 @@
         if utils.share_replica_quotas_are_supported():
             self.assertGreater(int(quotas["share_replicas"]), -2)
             self.assertGreater(int(quotas["replica_gigabytes"]), -2)
+        if utils.encryption_keys_quota_supported():
+            self.assertGreater(int(quotas["encryption_keys"]), -2)
 
     @decorators.idempotent_id('9b96dd45-7c0d-41ee-88e4-600185f61358')
     @tc.attr(base.TAG_POSITIVE, base.TAG_API)
@@ -471,6 +475,19 @@
 
         self.assertEqual(new_quota, int(updated["share_networks"]))
 
+    @decorators.idempotent_id('78957d97-afad-4371-a21e-79641fff83f7')
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API)
+    @utils.skip_if_microversion_not_supported("2.90")
+    def test_update_tenant_quota_encryption_keys(self):
+        # get current quotas
+        quotas = self.client.show_quotas(self.tenant_id)['quota_set']
+        new_quota = int(quotas["encryption_keys"]) + 2
+
+        # set new quota for encryption keys
+        updated = self.update_quotas(self.tenant_id, encryption_keys=new_quota)
+
+        self.assertEqual(new_quota, int(updated["encryption_keys"]))
+
     @decorators.idempotent_id('84e24c32-ee78-461e-ac1f-f9e4d99f88e2')
     @tc.attr(base.TAG_POSITIVE, base.TAG_API)
     def test_reset_tenant_quotas(self):
@@ -496,6 +513,8 @@
         if utils.share_replica_quotas_are_supported():
             data["share_replicas"] = int(custom["share_replicas"]) + 2
             data["replica_gigabytes"] = int(custom["replica_gigabytes"]) + 2
+        if utils.encryption_keys_quota_supported():
+            data["encryption_keys"] = int(custom["encryption_keys"]) + 2
 
         # set new quota, turn off cleanup - we'll do it right below
         updated = self.update_quotas(self.tenant_id, cleanup=False, **data)
@@ -518,6 +537,9 @@
                 data["share_replicas"], int(updated["share_replicas"]))
             self.assertEqual(
                 data["replica_gigabytes"], int(updated["replica_gigabytes"]))
+        if utils.encryption_keys_quota_supported():
+            self.assertEqual(
+                data["encryption_keys"], int(updated["encryption_keys"]))
 
         # Reset customized quotas
         self.client.reset_quotas(self.tenant_id)
@@ -545,6 +567,10 @@
             self.assertEqual(
                 int(default["replica_gigabytes"]),
                 int(reseted["replica_gigabytes"]))
+        if utils.encryption_keys_quota_supported():
+            self.assertEqual(
+                int(default["encryption_keys"]),
+                int(reseted["encryption_keys"]))
 
     def _get_new_replica_quota_values(self, default_quotas, value_to_set):
         new_values = {
diff --git a/manila_tempest_tests/tests/api/admin/test_quotas_negative.py b/manila_tempest_tests/tests/api/admin/test_quotas_negative.py
index 5b4ff1d..2534104 100644
--- a/manila_tempest_tests/tests/api/admin/test_quotas_negative.py
+++ b/manila_tempest_tests/tests/api/admin/test_quotas_negative.py
@@ -80,6 +80,7 @@
         {"gigabytes": -2},
         {"snapshot_gigabytes": -2},
         {"share_networks": -2},
+        {"encryption_keys": -2},
     )
     @decorators.idempotent_id('07d3e69a-7cda-4ca7-9fea-c32f6830fdd3')
     @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
diff --git a/manila_tempest_tests/tests/api/test_share_encryption.py b/manila_tempest_tests/tests/api/test_share_encryption.py
new file mode 100644
index 0000000..e199114
--- /dev/null
+++ b/manila_tempest_tests/tests/api/test_share_encryption.py
@@ -0,0 +1,94 @@
+# Copyright 2025 Cloudifcation GmbH.  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.
+
+import ddt
+from oslo_log import log
+from tempest import config
+from tempest.lib import decorators
+from testtools import testcase as tc
+
+from manila_tempest_tests.common import barbican_client_mgr
+from manila_tempest_tests.tests.api import base
+from manila_tempest_tests import utils
+
+CONF = config.CONF
+LOG = log.getLogger(__name__)
+
+
+@ddt.ddt
+class ShareEncryptionNFSTest(base.BaseSharesMixedTest):
+    """Covers share functionality, that is related to NFS share type."""
+    protocol = "nfs"
+
+    @classmethod
+    def skip_checks(cls):
+        super(ShareEncryptionNFSTest, cls).skip_checks()
+        if not CONF.share.run_encryption_tests:
+            raise cls.skipException('Encryption tests are disabled.')
+        utils.check_skip_if_microversion_not_supported("2.90")
+
+        if cls.protocol not in CONF.share.enable_protocols:
+            message = "%s tests are disabled" % cls.protocol
+            raise cls.skipException(message)
+
+        if ('share_server' not in CONF.share.capability_encryption_support and
+                'share' not in CONF.share.capability_encryption_support):
+            message = "Unsupported value of encryption support capability"
+            raise cls.skipException(message)
+
+    @classmethod
+    def resource_setup(cls):
+        super(ShareEncryptionNFSTest, cls).resource_setup()
+
+        extra_specs = {
+            'driver_handles_share_servers': CONF.share.multitenancy_enabled,
+        }
+        if 'share_server' in CONF.share.capability_encryption_support:
+            extra_specs.update({'encryption_support': 'share_server'})
+        elif 'share' in CONF.share.capability_encryption_support:
+            extra_specs.update({'encryption_support': 'share'})
+
+        # create share_type
+        cls.share_type_enc = cls.create_share_type(extra_specs=extra_specs)
+        cls.share_type_enc_id = cls.share_type_enc['id']
+
+        # setup barbican client
+        cls.barbican_mgr = barbican_client_mgr.BarbicanClientManager()
+        cls.barbican_mgr.setup_clients(cls.os_primary)
+
+    @decorators.idempotent_id('21ad41fb-04cf-493c-bc2f-66c80220898c')
+    @tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
+    def test_create_share_with_share_server_encryption_key_ref(self):
+
+        secret_href = self.barbican_mgr.store_secret()
+        secret_href_uuid = self.barbican_mgr.ref_to_uuid(secret_href)
+
+        share = self.create_share(
+            share_protocol=self.protocol,
+            share_type_id=self.share_type_enc_id,
+            share_network_id=self.shares_v2_client.share_network_id,
+            size=1,
+            name="encrypted_share",
+            encryption_key_ref=secret_href_uuid,
+            cleanup_in_class=False)
+
+        self.assertEqual(share['encryption_key_ref'], secret_href_uuid)
+
+        # Delete Barbican secret
+        self.barbican_mgr.delete_secret(secret_href_uuid)
+
+
+class ShareEncryptionCIFSTest(ShareEncryptionNFSTest):
+    """Covers share functionality, that is related to CIFS share type."""
+    protocol = "cifs"
diff --git a/manila_tempest_tests/tests/api/test_share_encryption_negative.py b/manila_tempest_tests/tests/api/test_share_encryption_negative.py
new file mode 100644
index 0000000..3d37426
--- /dev/null
+++ b/manila_tempest_tests/tests/api/test_share_encryption_negative.py
@@ -0,0 +1,74 @@
+# Copyright 2025 Cloudifcation GmbH.  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 import decorators
+from tempest.lib import exceptions as lib_exc
+from testtools import testcase as tc
+
+from manila_tempest_tests.tests.api import base
+from manila_tempest_tests import utils
+
+CONF = config.CONF
+
+
+class SharesEncryptionNegativeTest(base.BaseSharesMixedTest):
+
+    @classmethod
+    def skip_checks(cls):
+        super(SharesEncryptionNegativeTest, cls).skip_checks()
+        if not CONF.share.run_encryption_tests:
+            raise cls.skipException('Encryption tests are disabled.')
+        utils.check_skip_if_microversion_not_supported("2.90")
+
+    @classmethod
+    def resource_setup(cls):
+        super(SharesEncryptionNegativeTest, cls).resource_setup()
+        # create share_type
+        cls.no_encryption_type = cls.create_share_type()
+        cls.no_encryption_type_id = cls.no_encryption_type['id']
+        cls.encryption_type = cls.create_share_type(
+            extra_specs={
+                'encryption_support': 'share_server',
+            })
+        cls.encryption_type_id = cls.encryption_type['id']
+
+    @decorators.idempotent_id('b8097d56-067e-4d7c-8401-31bc7021fe81')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
+    def test_create_share_with_invalid_share_type(self):
+        # should not create share when encryption isn't supported by
+        # share type
+        self.assertRaises(lib_exc.BadRequest,
+                          self.shares_v2_client.create_share,
+                          share_type_id=self.no_encryption_type_id,
+                          encryption_key_ref='fake_ref')
+
+    @decorators.idempotent_id('b8097d56-067e-4d7c-8401-31bc7021fe88')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
+    def test_create_share_with_invalid_encryption_key_ref(self):
+        # should not create share when key ref is invalid UUID
+        self.assertRaises(lib_exc.BadRequest,
+                          self.shares_v2_client.create_share,
+                          share_type_id=self.encryption_type_id,
+                          encryption_key_ref='fake_ref')
+
+    @decorators.idempotent_id('b8097d56-067e-4d7c-8401-31bc7021fe82')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
+    def test_create_share_with_encryption_key_ref_absent_in_barbican(self):
+        # should not create share when key ref is not present in barbican
+        self.assertRaises(
+            lib_exc.BadRequest,
+            self.shares_v2_client.create_share,
+            share_type_id=self.encryption_type_id,
+            encryption_key_ref='cfbe8ae1-7932-43f2-bf82-3fd3ddba30c3')
diff --git a/manila_tempest_tests/utils.py b/manila_tempest_tests/utils.py
index d7d86f1..56a28e2 100644
--- a/manila_tempest_tests/utils.py
+++ b/manila_tempest_tests/utils.py
@@ -27,6 +27,7 @@
 CONF = config.CONF
 SHARE_NETWORK_SUBNETS_MICROVERSION = '2.51'
 SHARE_REPLICA_QUOTAS_MICROVERSION = "2.53"
+ENCRYPTION_KEYS_QUOTA_MICROVERSION = "2.90"
 EXPERIMENTAL = {'X-OpenStack-Manila-API-Experimental': 'True'}
 
 
@@ -276,6 +277,10 @@
     return is_microversion_supported(SHARE_REPLICA_QUOTAS_MICROVERSION)
 
 
+def encryption_keys_quota_supported():
+    return is_microversion_supported(ENCRYPTION_KEYS_QUOTA_MICROVERSION)
+
+
 def share_network_get_default_subnet(share_network):
     return next((
         subnet for subnet in share_network.get('share_network_subnets', [])