From 55064f3e039b1d0ff446fc78f8619741e7edaebe Mon Sep 17 00:00:00 2001 From: Parag Kamble Date: Fri, 13 Dec 2024 00:23:33 +0530 Subject: [PATCH] Test Automation for encryption dashboard summary Signed-off-by: Parag Kamble --- ocs_ci/ocs/constants.py | 7 + ocs_ci/ocs/ui/helpers_ui.py | 25 +++ .../ocs/ui/page_objects/encryption_module.py | 107 +++++++++++ .../ui/page_objects/storage_system_details.py | 6 +- ocs_ci/ocs/ui/views.py | 36 ++++ ...test_encryption_configuration_dashboard.py | 174 ++++++++++++++++++ 6 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 ocs_ci/ocs/ui/page_objects/encryption_module.py create mode 100644 tests/functional/encryption/test_encryption_configuration_dashboard.py diff --git a/ocs_ci/ocs/constants.py b/ocs_ci/ocs/constants.py index 97c62d4db6e..ffc0eedca42 100644 --- a/ocs_ci/ocs/constants.py +++ b/ocs_ci/ocs/constants.py @@ -3117,3 +3117,10 @@ MACHINE_POOL_ACTIONS = [CREATE, EDIT, DELETE] # MDR multicluster roles MDR_ROLES = ["ActiveACM", "PassiveACM", "PrimaryODF", "SecondaryODF"] + +ENCRYPTION_DASHBOARD_CONTEXT_MAP = { + "Cluster-wide encryption": "cluster_wide_encryption", + "Storage class encryption": "storageclass_encryption", + "In-transit encryption": "intransit_encryption", + "Object storage": "object_storage", +} diff --git a/ocs_ci/ocs/ui/helpers_ui.py b/ocs_ci/ocs/ui/helpers_ui.py index 60d1a50a7d9..ef68dc7589c 100644 --- a/ocs_ci/ocs/ui/helpers_ui.py +++ b/ocs_ci/ocs/ui/helpers_ui.py @@ -8,6 +8,7 @@ from ocs_ci.ocs.ui.base_ui import login_ui, close_browser from ocs_ci.ocs.ui.add_replace_device_ui import AddReplaceDeviceUI from ocs_ci.ocs.resources.storage_cluster import get_deviceset_count, get_osd_size +from ocs_ci.ocs.exceptions import ResourceNotFoundError logger = logging.getLogger(__name__) @@ -217,3 +218,27 @@ def is_ui_deployment(): return True return False + + +def extract_encryption_status(root_element, svg_path): + """Function to extract encryption status from an SVG element + + Args: + root_element (str): Dom root element + svg_element (str): svg element path + + Returns: + bool: if encryption status is enable for given element return True otherwise False. + + Raises: + ResourceNotFoundError: If given resource is not found. + """ + try: + svg_element = root_element.find_element(By.CSS_SELECTOR, svg_path) + if svg_element and svg_element.tag_name == "svg": + if svg_element.get_attribute("data-test") == "success-icon": + return True + else: + return False + except Exception as e: + raise ResourceNotFoundError(f"Given SVG element is not Found: {e}") diff --git a/ocs_ci/ocs/ui/page_objects/encryption_module.py b/ocs_ci/ocs/ui/page_objects/encryption_module.py new file mode 100644 index 00000000000..b82e5bc114c --- /dev/null +++ b/ocs_ci/ocs/ui/page_objects/encryption_module.py @@ -0,0 +1,107 @@ +from ocs_ci.ocs.ui.helpers_ui import logger +from ocs_ci.ocs.constants import ENCRYPTION_DASHBOARD_CONTEXT_MAP +from ocs_ci.ocs.ui.page_objects.page_navigator import PageNavigator + + +class EncryptionModule(PageNavigator): + def _get_encryption_summary(self, context_key): + """ + Generic method to collect encryption summary based on the context. + + Args: + context_key (str): Key to determine the validation location. + + Returns: + dict: Encryption summary for the given context. + """ + encryption_summary = { + "object_storage": {"status": False, "kms": False}, + "cluster_wide_encryption": {"status": False, "kms": False}, + "storageclass_encryption": {"status": False, "kms": False}, + "intransit_encryption": {"status": False}, + } + + logger.info(f"Getting Encryption Summary for context: {context_key}") + + # Open the encryption summary popup + self.do_click( + self.validation_loc["encryption_summary"][context_key]["enabled"], + enable_screenshot=True, + ) + + self.page_has_loaded( + module_loc=self.validation_loc["encryption_summary"][context_key][ + "encryption_content_data" + ] + ) + + # Get the root element for encryption details + encryption_content_location = self.validation_loc["encryption_summary"][ + context_key + ]["encryption_content_data"] + root_elements = self.get_elements(encryption_content_location) + + if not root_elements: + raise ValueError("Error getting root web element") + root_element = root_elements[0] + + # Extract headers and statuses + enc_headers = [ + head + for head in root_element.find_elements_by_tag_name("h6") + if head.text in ENCRYPTION_DASHBOARD_CONTEXT_MAP + ] + enc_status = [ + svg + for svg in root_element.find_elements_by_tag_name("svg") + if svg.get_attribute("color") + ] + + for header, svg in zip(enc_headers, enc_status): + context = ENCRYPTION_DASHBOARD_CONTEXT_MAP[header.text] + encryption_summary[context]["status"] = ( + svg.get_attribute("color") == "#3e8635" + ) + + # Process encryption summary text + current_context = None + encryption_summary_text = self.get_element_text(encryption_content_location) + + for line in map(str.strip, encryption_summary_text.split("\n")): + if line in ENCRYPTION_DASHBOARD_CONTEXT_MAP: + current_context = ENCRYPTION_DASHBOARD_CONTEXT_MAP[line] + elif current_context and current_context in encryption_summary: + encryption_summary[current_context]["kms"] = ( + line.split(":")[-1].strip() + if "External Key Management Service" in line + else False + ) + + logger.info(f"Encryption Summary for {context_key}: {encryption_summary}") + + # Close the popup + logger.info("Closing the popup") + self.do_click( + self.validation_loc["encryption_summary"][context_key]["close"], + enable_screenshot=True, + ) + + return encryption_summary + + def get_object_encryption_summary(self): + """ + Retrieve the encryption summary for the object details page. + + Returns: + dict: Encryption summary on object details page. + """ + return self._get_encryption_summary("object_storage") + + def get_block_file_encryption_summary(self): + """ + Retrieve the encryption summary for the block and file page. + + Returns: + dict: Encryption summary on block and file page. + """ + return self._get_encryption_summary("file_and_block") diff --git a/ocs_ci/ocs/ui/page_objects/storage_system_details.py b/ocs_ci/ocs/ui/page_objects/storage_system_details.py index 3922c295ee6..afde9efe945 100644 --- a/ocs_ci/ocs/ui/page_objects/storage_system_details.py +++ b/ocs_ci/ocs/ui/page_objects/storage_system_details.py @@ -2,11 +2,13 @@ from ocs_ci.ocs.ui.base_ui import logger, BaseUI from ocs_ci.ocs.ui.page_objects.storage_system_tab import StorageSystemTab from ocs_ci.utility import version +from ocs_ci.ocs.ui.page_objects.encryption_module import EncryptionModule -class StorageSystemDetails(StorageSystemTab): +class StorageSystemDetails(StorageSystemTab, EncryptionModule): def __init__(self): StorageSystemTab.__init__(self) + EncryptionModule.__init__(self) def nav_details_overview(self): logger.info("Click on Overview tab") @@ -33,6 +35,8 @@ def nav_details_object(self): else: self.do_click(self.validation_loc["object"], enable_screenshot=True) + return self + def nav_block_and_file(self): """ Accessible only at StorageSystems / StorageSystem details / Overview diff --git a/ocs_ci/ocs/ui/views.py b/ocs_ci/ocs/ui/views.py index 87aab0c4cc2..8f9fd2fb1d2 100644 --- a/ocs_ci/ocs/ui/views.py +++ b/ocs_ci/ocs/ui/views.py @@ -1794,6 +1794,41 @@ ), } +validation_4_18 = { + "encryption_summary": { + "file_and_block": { + "enabled": ( + "//button[@class='pf-v5-c-button pf-m-link pf-m-inline' and " + "(text()='Enabled' or text()='Not enabled')]", + By.XPATH, + ), + "close": ( + "//button[@class='pf-v5-c-button pf-m-plain' and @aria-label='Close']", + By.XPATH, + ), + "encryption_content_data": ( + "//div[@class='pf-v5-c-popover__body']", + By.XPATH, + ), + }, + "object_storage": { + "enabled": ( + "//button[@class='pf-v5-c-button pf-m-link pf-m-inline' and " + "(text()='Enabled' or text()='Not enabled')]", + By.XPATH, + ), + "close": ( + "//button[@class='pf-v5-c-button pf-m-plain' and @aria-label='Close']", + By.XPATH, + ), + "encryption_content_data": ( + "//div[@class='pf-v5-c-popover__content']", + By.XPATH, + ), + }, + } +} + topology = { "topology_graph": ("//*[@data-kind='graph']", By.XPATH), "node_label": ("//*[@class='pf-topology__node__label']", By.XPATH), @@ -2035,6 +2070,7 @@ **validation_4_13, **validation_4_14, **validation_4_17, + **validation_4_18, }, "block_pool": {**block_pool, **block_pool_4_12, **block_pool_4_13}, "storageclass": {**storageclass, **storageclass_4_9}, diff --git a/tests/functional/encryption/test_encryption_configuration_dashboard.py b/tests/functional/encryption/test_encryption_configuration_dashboard.py new file mode 100644 index 00000000000..0515edb5618 --- /dev/null +++ b/tests/functional/encryption/test_encryption_configuration_dashboard.py @@ -0,0 +1,174 @@ +import pytest +import logging + +log = logging.getLogger(__name__) +from ocs_ci.ocs.ui.page_objects.page_navigator import PageNavigator +from ocs_ci.framework.pytest_customization.marks import green_squad, tier1 +from ocs_ci.ocs.ocp import OCP +from ocs_ci.framework import config +from ocs_ci.ocs import constants +from ocs_ci.ocs.resources.storage_cluster import StorageCluster +from ocs_ci.helpers.helpers import storagecluster_independent_check + + +@green_squad +@tier1 +class TestEncryptionConfigurationDashboard: + @pytest.fixture(autouse=True) + def encryption_status(self): + """ + Collect Encryption status from storagecluster and noobaa spec. + """ + # Retrieve encryption details + cluster_name = ( + constants.DEFAULT_CLUSTERNAME_EXTERNAL_MODE + if storagecluster_independent_check() + else constants.DEFAULT_CLUSTERNAME + ) + + sc_obj = StorageCluster( + resource_name=cluster_name, + namespace=config.ENV_DATA["cluster_namespace"], + ) + + self.enc_details = sc_obj.data["spec"].get("encryption", {}) + self.intransit_encryption_status = ( + sc_obj.data["spec"] + .get("network", {}) + .get("connections", {}) + .get("encryption", {}) + .get("enabled", False) + ) + log.info(f"Encryption details from storagecluster Spec: {self.enc_details}") + + noobaa_obj = OCP( + kind="noobaa", + namespace=config.ENV_DATA["cluster_namespace"], + resource_name="noobaa", + ) + + self.noobaa_kms = ( + noobaa_obj.data["spec"] + .get("security", {}) + .get("kms", {}) + .get("connectionDetails", {}) + .get("KMS_PROVIDER", None) # Provide a default value of None if not found + ) + log.info(f"Noobaa Spec has mentioned KMS: {self.noobaa_kms}") + + def validate_encryption( + self, context, actual_status, expected_status, error_message + ): + """Helper function to validate encryption details + + Args: + context (str): Encryption Type + actual_status (str): Encryption status in the spec file + expected_status (str): Encryption status shown on the dashboard. + error_message (str): Error message to display. + """ + assert actual_status == expected_status, error_message + log.info(f"{context} status is as expected: {actual_status}") + + @pytest.mark.polarion_id("OCS-6300") + def test_file_block_encryption_configuration_dashboard(self, setup_ui_class): + """Test the encryption configuration dashboard of File And Block details for correctness. + + Steps: + 1. Navigate to file and block details page + 2. Open encryption details. + 3. verify encryption data with the nooba and storagecluster spec. + """ + + # Navigate to the block and file page + block_and_file_page = ( + PageNavigator() + .nav_odf_default_page() + .nav_storage_systems_tab() + .nav_storagecluster_storagesystem_details() + .nav_block_and_file() + ) + + # Retrieve encryption summary from the dashboard + encryption_summary = block_and_file_page.get_block_file_encryption_summary() + + # Validate cluster-wide encryption + cluster_wide_details = self.enc_details.get("clusterWide", {}) + if isinstance(cluster_wide_details, dict): + self.validate_encryption( + "ClusterWide Encryption", + encryption_summary["cluster_wide_encryption"]["status"], + cluster_wide_details.get("status", False), + "ClusterWide Encryption is not showing correctly in the dashboard.", + ) + self.validate_encryption( + "ClusterWide KMS", + encryption_summary["cluster_wide_encryption"]["kms"], + cluster_wide_details.get("kms", {}).get("enable", False), + "KMS is not mentioned in the encryption summary.", + ) + else: + log.warning( + "ClusterWide Encryption details are not a dictionary, skipping checks." + ) + + # Validate storage class encryption + storage_class_details = self.enc_details.get("storageClass", {}) + if isinstance(storage_class_details, dict): + self.validate_encryption( + "StorageClass Encryption", + encryption_summary["storageclass_encryption"]["status"], + storage_class_details.get("status", False), + "StorageClass encryption is not showing correctly in the dashboard.", + ) + else: + log.warning("StorageClass details are not a dictionary, skipping checks.") + + # Validate in-transit encryption + self.validate_encryption( + "InTransit Encryption", + encryption_summary["intransit_encryption"]["status"], + self.intransit_encryption_status, + "InTransit Encryption status is incorrect in the dashboard.", + ) + + @pytest.mark.polarion_id("OCS-6301") + def test_object_storage_encryption_configuration_dashboard(self, setup_ui_class): + """Test the encryption configuration dashboard of Object details for correctness. + + Steps: + 1. Navigate to object details page + 2. Open encryption details. + 3. verify encryption data with the nooba and storagecluster spec. + """ + # Navigate to the Object Storage page + object_details_page = ( + PageNavigator() + .nav_odf_default_page() + .nav_storage_systems_tab() + .nav_storagecluster_storagesystem_details() + .nav_details_object() + ) + + encryption_summary = object_details_page.get_object_encryption_summary() + log.info(f"Encryption Summary from page : {encryption_summary}") + + # Validate Object Encryption Summary + assert encryption_summary["object_storage"][ + "status" + ], "Object encryption summary is wrong" + + if bool(encryption_summary["object_storage"]["kms"]): + log.info("Verifying object_storage KMS status") + assert ( + encryption_summary["object_storage"]["kms"].upper() + == self.noobaa_kms.upper() + ), "KMS details is not correct" + + # Validate in-transit encryption + self.validate_encryption( + "InTransit Encryption", + encryption_summary["intransit_encryption"]["status"], + self.intransit_encryption_status, + "InTransit Encryption status is incorrect in the dashboard.", + )