From d49a15a696555fa48d3c35c2454bce0e29225d4e Mon Sep 17 00:00:00 2001 From: AquaD17 Date: Fri, 6 Feb 2026 18:01:25 +0530 Subject: [PATCH 01/13] [patch] patch ingress configure before the installation --- python/src/mas/cli/install/app.py | 18 ++++++++++++++---- python/src/mas/cli/install/summarizer.py | 2 +- tekton/src/params/install.yml.j2 | 4 ---- .../taskdefs/core/suite-install.yml.j2 | 2 -- tekton/src/tasks/suite-install.yml.j2 | 6 ------ 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/python/src/mas/cli/install/app.py b/python/src/mas/cli/install/app.py index 4f6fbdf6f86..a8835cdff0e 100644 --- a/python/src/mas/cli/install/app.py +++ b/python/src/mas/cli/install/app.py @@ -50,7 +50,8 @@ createNamespace, getStorageClasses, getClusterVersion, - isClusterVersionInRange + isClusterVersionInRange, + configureIngressForPathBasedRouting ) from mas.devops.mas import ( getCurrentCatalog, @@ -668,13 +669,13 @@ def configRoutingMode(self): " routeAdmission:", " namespaceOwnership: InterNamespaceAllowed", "", - "Would you like to configure it during installation?" + "Would you like to configure it now (before MAS installation)?" ]) if self.yesOrNo("Configure IngressController for path-based routing"): self.setParam("mas_routing_mode", "path") self.setParam("mas_configure_ingress", "true") - self.printDescription([f"IngressController '{selectedController}' will be configured during installation."]) + self.printDescription([f"IngressController '{selectedController}' will be configured before MAS installation begins."]) else: self.printDescription([ "", @@ -1799,7 +1800,7 @@ def install(self, argv): ) elif not isConfigured: if hasattr(self.args, 'mas_configure_ingress') and self.args.mas_configure_ingress: - logger.info(f"IngressController '{ingressControllerName}' will be configured for path-based routing during installation pipeline") + logger.info(f"IngressController '{ingressControllerName}' will be configured for path-based routing before MAS installation") self.setParam("mas_configure_ingress", "true") else: self.fatalError( @@ -1859,6 +1860,15 @@ def install(self, argv): h.stop_and_persist(symbol=self.successIcon, text="OpenShift Pipelines Operator installation failed") self.fatalError("Installation failed") + if self.getParam("mas_routing_mode") == "path" and self.getParam("mas_configure_ingress") == "true": + with Halo(text='Configuring cluster for path-based routing', spinner=self.spinner) as h: + ingressControllerName = self.getParam("mas_ingress_controller_name") if self.getParam("mas_ingress_controller_name") else "default" + if configureIngressForPathBasedRouting(self.dynamicClient, ingressControllerName): + h.stop_and_persist(symbol=self.successIcon, text="Cluster configured for path-based routing") + else: + h.stop_and_persist(symbol=self.failureIcon, text="Failed to configure cluster for path-based routing") + self.fatalError("Installation failed - unable to configure IngressController for path-based routing") + with Halo(text=f'Preparing namespace ({pipelinesNamespace})', spinner=self.spinner) as h: createNamespace(self.dynamicClient, pipelinesNamespace) preparePipelinesNamespace( diff --git a/python/src/mas/cli/install/summarizer.py b/python/src/mas/cli/install/summarizer.py index ba74561de71..bc1d1663df4 100644 --- a/python/src/mas/cli/install/summarizer.py +++ b/python/src/mas/cli/install/summarizer.py @@ -79,7 +79,7 @@ def masSummary(self) -> None: self.printParamSummary("Network Routing Mode", "mas_routing_mode") if self.getParam("mas_routing_mode") == "path": self.printParamSummary("IngressController Name", "mas_ingress_controller_name") - self.printParamSummary("Configure IngressController", "mas_configure_ingress") + self.printParamSummary("Configure IngressController for path routing", "mas_configure_ingress") print() self.printParamSummary("Configure Suite to run in IPV6", "enable_ipv6") diff --git a/tekton/src/params/install.yml.j2 b/tekton/src/params/install.yml.j2 index 32a24709146..b389f2a8aa2 100644 --- a/tekton/src/params/install.yml.j2 +++ b/tekton/src/params/install.yml.j2 @@ -443,10 +443,6 @@ type: string default: "" description: Name of the IngressController to use for path-based routing -- name: mas_configure_ingress - type: string - default: "" - description: Set to 'true' to automatically configure IngressController for path-based routing - name: mas_trust_default_cas type: string default: "" diff --git a/tekton/src/pipelines/taskdefs/core/suite-install.yml.j2 b/tekton/src/pipelines/taskdefs/core/suite-install.yml.j2 index 980083e483d..c29a00dfd4f 100644 --- a/tekton/src/pipelines/taskdefs/core/suite-install.yml.j2 +++ b/tekton/src/pipelines/taskdefs/core/suite-install.yml.j2 @@ -58,8 +58,6 @@ value: $(params.mas_routing_mode) - name: mas_ingress_controller_name value: $(params.mas_ingress_controller_name) - - name: mas_configure_ingress - value: $(params.mas_configure_ingress) - name: mas_manual_cert_mgmt value: $(params.mas_manual_cert_mgmt) - name: mas_trust_default_cas diff --git a/tekton/src/tasks/suite-install.yml.j2 b/tekton/src/tasks/suite-install.yml.j2 index 376c373a9e9..c2217a754c2 100644 --- a/tekton/src/tasks/suite-install.yml.j2 +++ b/tekton/src/tasks/suite-install.yml.j2 @@ -51,10 +51,6 @@ spec: type: string description: Name of the IngressController to use for path-based routing default: "" - - name: mas_configure_ingress - type: string - description: Set to 'true' to automatically configure IngressController for path-based routing - default: "False" - name: mas_trust_default_cas type: string description: Optional boolean parameter that when set to False, disables the normal trust of well known public certificate authorities @@ -166,8 +162,6 @@ spec: value: $(params.mas_routing_mode) - name: MAS_INGRESS_CONTROLLER_NAME value: $(params.mas_ingress_controller_name) - - name: MAS_CONFIGURE_INGRESS - value: $(params.mas_configure_ingress) - name: MAS_MANUAL_CERT_MGMT value: $(params.mas_manual_cert_mgmt) - name: MAS_TRUST_DEFAULT_CAS From 8cf90ce8359dc17d129245937b87ccbca5c2f9d3 Mon Sep 17 00:00:00 2001 From: AquaD17 Date: Fri, 6 Feb 2026 18:02:26 +0530 Subject: [PATCH 02/13] [patch] add python devops changes --- image/cli/install/mas_devops.tar.gz | Bin 0 -> 82168 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 image/cli/install/mas_devops.tar.gz diff --git a/image/cli/install/mas_devops.tar.gz b/image/cli/install/mas_devops.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..bd7ae68e75234b3a7b0c8183e92e8f5c8135257c GIT binary patch literal 82168 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
    Ae8J1J-IoSzG@)D!lyTkWXOu#(%1z&;*bCw? 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 U^go-QZv_7IA6-o#KUkpu17AV4rvLx| literal 0 HcmV?d00001 From 7f23babb672b4dd6c146c71cf9ff662830eac20f Mon Sep 17 00:00:00 2001 From: AquaD17 Date: Fri, 6 Feb 2026 19:45:29 +0530 Subject: [PATCH 03/13] remove the mas_Devops --- image/cli/install/mas_devops.tar.gz | Bin 82168 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 image/cli/install/mas_devops.tar.gz diff --git a/image/cli/install/mas_devops.tar.gz b/image/cli/install/mas_devops.tar.gz deleted file mode 100644 index bd7ae68e75234b3a7b0c8183e92e8f5c8135257c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82168 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
      Ae8J1J-IoSzG@)D!lyTkWXOu#(%1z&;*bCw? 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 U^go-QZv_7IA6-o#KUkpu17AV4rvLx| From 4c168642e150336254a744794d08c057415d7900 Mon Sep 17 00:00:00 2001 From: AquaD17 Date: Mon, 9 Feb 2026 14:24:26 +0530 Subject: [PATCH 04/13] fix the pre-commit failure --- python/src/mas/cli/install/app.py | 141 +++++++++++++++++++++++------ rbac/install/user/clusterrole.yaml | 20 ++++ 2 files changed, 135 insertions(+), 26 deletions(-) diff --git a/python/src/mas/cli/install/app.py b/python/src/mas/cli/install/app.py index 5528f400759..117daacfa81 100644 --- a/python/src/mas/cli/install/app.py +++ b/python/src/mas/cli/install/app.py @@ -546,9 +546,9 @@ def configMAS(self): self.setParam("sls_namespace", f"mas-{self.getParam('mas_instance_id')}-sls") self.configOperationMode() - self.configRoutingMode() self.configCATrust() self.configDNSAndCerts() + self.configRoutingMode() self.configSSOProperties() self.configSpecialCharacters() self.configGuidedTour() @@ -587,6 +587,25 @@ def configOperationMode(self): self.setParam("aiservice_rhoai_model_deployment_type", "serverless") self.setParam("rhoai", "false") + def _getMasDomainForDisplay(self): + masDomain = self.getParam("mas_domain") + if not masDomain: + try: + ingressAPI = self.dynamicClient.resources.get( + api_version="config.openshift.io/v1", + kind="Ingress" + ) + ingressConfig = ingressAPI.get(name="cluster") + masDomain = ingressConfig.spec.get('domain', 'yourdomain.com') + except Exception: + masDomain = 'yourdomain.com' + + masInstanceId = self.getParam("mas_instance_id") + if masInstanceId: + masDomain = f"{masInstanceId}.{masDomain}" + + return masDomain + def _promptForIngressController(self): try: ingressControllerAPI = self.dynamicClient.resources.get( @@ -639,11 +658,23 @@ def configRoutingMode(self): if self.showAdvancedOptions and isVersionEqualOrAfter('9.2.0', self.getParam("mas_channel")) and self.getParam("mas_channel") != '9.2.x-feature': self.printH1("Configure Routing Mode") + masDomain = self._getMasDomainForDisplay() + self.printDescription([ - "Maximo Application Suite can be installed so it can be accessed with single domain URLs (path mode) or multi-domain URLs (subdomain mode):", + "Maximo Application Suite can be configured in one of two ways:", + "", + " 1. Single domain with path-based routing across the suite", + f" Example: https://{masDomain}/admin", "", - " 1. Path (single domain)", - " 2. Subdomain (multi domain)" + " 2. Multi domain with subdomain-based routing across the suite", + f" Example: https://admin.{masDomain}", + "", + "Path-based routing requires the IngressController to have the routeAdmission policy", + "set to 'InterNamespaceAllowed'. This allows routes to claim the same hostname across", + "different namespaces, which is necessary for path-based routing to function correctly.", + "", + "For more information refer to:", + "https://docs.redhat.com/en/documentation/openshift_container_platform/4.20/html/ingress_and_load_balancing/routes#nw-route-admission-policy_configuring-routes" ]) routingModeInt = self.promptForInt("Routing Mode", default=1, min=1, max=2) @@ -651,43 +682,58 @@ def configRoutingMode(self): selectedMode = routingModeOptions[routingModeInt - 1] if selectedMode == "path": - selectedController = self._promptForIngressController() - self.setParam("mas_ingress_controller_name", selectedController) - - # Check if selected IngressController is configured for path-based routing - # Note: In interactive mode, we only check configuration, not existence, - # since the user selects from a list of existing controllers - _, isConfigured = self._checkIngressControllerForPathRouting(selectedController) - - if isConfigured: - self.setParam("mas_routing_mode", "path") - self.printDescription([f" IngressController '{selectedController}' is configured for path-based routing."]) - else: + canConfigure = self._checkIngressControllerPermissions() + if not canConfigure: self.printDescription([ "", - f"The IngressController '{selectedController}' requires configuration for path-based routing.", + "Your cluster ingress currently does not support path-based routing", "", - "The following setting needs to be applied:", + "If you wish to configure MAS with path-based routing, contact your OpenShift", + "administrator to apply the following configuration:", "", " spec:", " routeAdmission:", " namespaceOwnership: InterNamespaceAllowed", "", - "Would you like to configure it now (before MAS installation)?" + "MAS will be configured to use subdomain-based routing." ]) + self.setParam("mas_routing_mode", "subdomain") + self.setParam("mas_ingress_controller_name", "") + else: + selectedController = self._promptForIngressController() + self.setParam("mas_ingress_controller_name", selectedController) - if self.yesOrNo("Configure IngressController for path-based routing"): + # Check if selected IngressController is configured for path-based routing + _, isConfigured = self._checkIngressControllerForPathRouting(selectedController) + + if isConfigured: self.setParam("mas_routing_mode", "path") - self.setParam("mas_configure_ingress", "true") - self.printDescription([f"IngressController '{selectedController}' will be configured before MAS installation begins."]) + self.printDescription([f"IngressController '{selectedController}' is configured for path-based routing."]) else: self.printDescription([ "", - "Path-based routing requires IngressController configuration.", - "Falling back to subdomain mode." + "Your cluster ingress currently does not support path-based routing", + "", + "The following setting needs to be applied to the IngressController:", + "", + " spec:", + " routeAdmission:", + " namespaceOwnership: InterNamespaceAllowed", + "" ]) - self.setParam("mas_routing_mode", "subdomain") - self.setParam("mas_ingress_controller_name", "") + + if self.yesOrNo("Configure ingress namespace ownership policy to enable path-based routing for MAS"): + self.setParam("mas_routing_mode", "path") + self.setParam("mas_configure_ingress", "true") + self.printDescription([f"IngressController '{selectedController}' will be configured before MAS installation begins."]) + else: + self.printDescription([ + "", + "Path-based routing requires IngressController configuration.", + "MAS will be configured to use subdomain-based routing." + ]) + self.setParam("mas_routing_mode", "subdomain") + self.setParam("mas_ingress_controller_name", "") else: self.setParam("mas_routing_mode", "subdomain") @@ -727,6 +773,26 @@ def _checkIngressControllerForPathRouting(self, controllerName='default'): logger.warning(f"Failed to check IngressController '{controllerName}' configuration: {e}") return (False, False) + def _checkIngressControllerPermissions(self, controllerName='default'): + try: + ingressControllerAPI = self.dynamicClient.resources.get( + api_version="operator.openshift.io/v1", + kind="IngressController" + ) + + # Attempt to get the IngressController to verify permissions + ingressControllerAPI.get( + name=controllerName, + namespace="openshift-ingress-operator" + ) + + logger.info(f"User has permissions to access IngressController '{controllerName}'") + return True + + except Exception as e: + logger.warning(f"User may not have permissions to configure IngressController '{controllerName}': {e}") + return False + @logMethodCall def configAnnotations(self): if self.operationalMode == 2: @@ -1776,6 +1842,29 @@ def install(self, argv): self.setParam("mas_ingress_controller_name", ingressControllerName) + # Check permissions BEFORE attempting to check the IngressController + canConfigure = self._checkIngressControllerPermissions() + if not canConfigure: + + self.fatalError( + "\n".join([ + "IngressController Configuration Requires Administrator Permissions", + "========================================================================", + "You do not have sufficient permissions to check or configure the", + f"IngressController '{ingressControllerName}'.", + "", + "If you wish to configure MAS with path-based routing, contact your OpenShift", + "administrator to apply the following configuration:", + "", + " spec:", + " routeAdmission:", + " namespaceOwnership: InterNamespaceAllowed", + "", + "Alternatively, you can use subdomain routing mode:", + " mas install --routing subdomain ..." + ]) + ) + exists, isConfigured = self._checkIngressControllerForPathRouting(ingressControllerName) if not exists: diff --git a/rbac/install/user/clusterrole.yaml b/rbac/install/user/clusterrole.yaml index 36d4011a2e4..50dbccdaf79 100644 --- a/rbac/install/user/clusterrole.yaml +++ b/rbac/install/user/clusterrole.yaml @@ -76,6 +76,26 @@ rules: verbs: - list + # The CLI needs to inspect and optionally configure IngressController for path-based routing + - apiGroups: + - operator.openshift.io + resources: + - ingresscontrollers + verbs: + - get + - list + - patch + + # The CLI needs to read the cluster ingress configuration to determine the domain + - apiGroups: + - config.openshift.io + resources: + - ingresses + verbs: + - get + resourceNames: + - cluster + # We need to set up the "pipeline" service account that will be used by the pipelines - apiGroups: - rbac.authorization.k8s.io From a31cc8878defc04047a9edef95be8b8767c9d95e Mon Sep 17 00:00:00 2001 From: AquaD17 Date: Wed, 11 Feb 2026 13:26:53 +0530 Subject: [PATCH 05/13] add test cases for the routing mode --- python/test/install/test_dev_mode.py | 224 ++++++ python/test/install/test_routing_mode.py | 977 +++++++++++++++++++++++ python/test/utils/install_test_helper.py | 93 ++- 3 files changed, 1276 insertions(+), 18 deletions(-) create mode 100644 python/test/install/test_routing_mode.py diff --git a/python/test/install/test_dev_mode.py b/python/test/install/test_dev_mode.py index e084d11213a..222c12e0b83 100644 --- a/python/test/install/test_dev_mode.py +++ b/python/test/install/test_dev_mode.py @@ -207,6 +207,143 @@ def test_install_master_dev_mode_existing_catalog(tmpdir): run_install_test(tmpdir, config) +def test_install_master_dev_mode_with_path_routing(tmpdir): + """Test interactive installation with 9.2.0 channel including path-based routing mode configuration. + + This test verifies the complete routing mode flow including IngressController configuration: + - Mock IngressController is initially NOT configured (namespaceOwnership='Strict') + - User selects path-based routing mode + - CLI detects IngressController needs configuration + - User agrees to configure it + - IngressController will be patched during installation + + Flow: + 1. User selects path-based routing mode + 2. CLI checks permissions (mocked to succeed) + 3. CLI auto-selects the only available IngressController ('default') + 4. CLI detects IngressController is NOT configured for path-based routing + 5. User is prompted to configure it + 6. User agrees (responds 'y') + 7. mas_configure_ingress parameter is set to 'true' + 8. IngressController will be patched during installation + """ + + # Define prompt handlers with expected patterns and responses + prompt_handlers = { + # 1. Cluster connection + '.*Proceed with this cluster?.*': lambda msg: 'y', + # 2. Install flavour (advanced options) - MUST be 'y' to enable routing mode + '.*Show advanced installation options.*': lambda msg: 'y', + # 3. Catalog selection + '.*Select catalog.*': lambda msg: "v9-master-amd64", + '.*Select channel.*': lambda msg: '9.2.x-dev', # Use 9.2.x-dev channel + # 4. Routing Mode Configuration - Select path-based routing + '.*Routing Mode.*': lambda msg: '1', # Select path-based routing + # Note: IngressController selection prompt does NOT appear because there's only one controller + # 5. Configure IngressController for path-based routing + '.*Configure ingress namespace ownership.*': lambda msg: 'y', # Agree to configure + # 5. Storage classes + ".*Use the auto-detected storage classes.*": lambda msg: 'y', + # 6. SLS configuration + '.*SLS Mode.*': lambda msg: '1', # SLS Mode prompt (appears with advanced options) + '.*SLS channel.*': lambda msg: '1.x-stable', + '.*License file.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', + # 7. DRO configuration + '.*DRO.*Namespace.*': lambda msg: '', # DRO Namespace prompt (appears with advanced options) + ".*Contact e-mail address.*": lambda msg: 'maximo@ibm.com', + ".*Contact first name.*": lambda msg: 'Test', + ".*Contact last name.*": lambda msg: 'Test', + # 8. ICR & Artifactory credentials + ".*IBM entitlement key.*": lambda msg: 'testEntitlementKey', + ".*Artifactory username.*": lambda msg: 'testUsername', + ".*Artifactory token.*": lambda msg: 'testToken', + # 9. MAS Instance configuration + '.*Instance ID.*': lambda msg: 'testinst', + '.*Workspace ID.*': lambda msg: 'testws', + '.*Workspace.*name.*': lambda msg: 'Test Workspace', + # 10. Operational mode + '.*Operational Mode.*': lambda msg: '1', + # 11. Certificate Authority Trust + '.*Trust default CAs.*': lambda msg: 'y', + # 12. Cluster ingress certificate secret name + '.*Cluster ingress certificate secret name.*': lambda msg: '', # Leave empty for auto-detection + # 13. Domain & certificate management + '.*Configure domain.*certificate management.*': lambda msg: 'n', # Skip domain/cert config for simplicity + # 14. SSO properties + '.*Configure SSO properties.*': lambda msg: 'n', # Skip SSO config + # 15. Special characters for user IDs + '.*Allow special characters for user IDs and usernames.*': lambda msg: 'n', + # 16. Guided Tour + '.*Enable Guided Tour.*': lambda msg: 'y', + # 17. Feature adoption metrics + '.*Enable feature adoption metrics.*': lambda msg: 'y', + # 18. Deployment progression metrics + '.*Enable deployment progression metrics.*': lambda msg: 'y', + # 19. Usability metrics + '.*Enable usability metrics.*': lambda msg: 'y', + # 20. Application selection + '.*Install IoT.*': lambda msg: 'y', + '.*Custom channel for iot.*': lambda msg: '9.2.x-dev', + '.*Install Monitor.*': lambda msg: 'n', + '.*Install Manage.*': lambda msg: 'y', + '.*Custom channel for manage.*': lambda msg: '9.2.x-dev', + '.*Select a server bundle configuration.*': lambda msg: '1', # Select dev server bundle + '.*Customize database settings.*': lambda msg: 'n', # Skip database customization + '.*Create demo data.*': lambda msg: 'n', # Skip demo data + '.*Manage server timezone.*': lambda msg: 'GMT', # Use GMT timezone + '.*Base language.*': lambda msg: 'EN', # Use English as base language + '.*Secondary language.*': lambda msg: '', # No secondary language + '.*Select components to enable.*': lambda msg: 'n', + '.*Include customization archive.*': lambda msg: 'n', + '.*Install Predict.*': lambda msg: 'n', + '.*Install Assist.*': lambda msg: 'n', + '.*Install Optimizer.*': lambda msg: 'n', + '.*Install Visual Inspection.*': lambda msg: 'n', + '.*Install.*Real Estate and Facilities.*': lambda msg: 'n', + '.*Install AI Service.*': lambda msg: 'n', + # 21. MongoDB configuration + '.*MongoDb namespace.*': lambda msg: 'mongoce', # Use default MongoDB namespace + '.*Create MongoDb cluster.*': lambda msg: 'y', + # 22. Db2 configuration + '.*Create system Db2 instance.*': lambda msg: 'y', + '.*Re-use System Db2 instance for Manage application.*': lambda msg: 'n', + '.*Create Manage dedicated Db2 instance.*': lambda msg: 'y', + '.*Select the Manage dedicated DB2 instance type.*': lambda msg: '1', # Select default DB2 type + '.*Install namespace.*': lambda msg: 'db2u', # DB2 install namespace + '.*Configure node affinity.*': lambda msg: 'n', # Skip node affinity configuration + '.*Configure node tolerations.*': lambda msg: 'n', # Skip node tolerations configuration + '.*Customize CPU and memory request/limit.*': lambda msg: 'n', # Skip CPU/memory customization + '.*Customize storage capacity.*': lambda msg: 'n', # Skip storage capacity customization + '.*Select Kafka provider.*': lambda msg: '1', # Select default Kafka provider + '.*Strimzi namespace.*': lambda msg: 'strimzi', # Strimzi namespace + '.*Use pod templates.*': lambda msg: 'n', # Skip pod templates + # 23. Kafka configuration + '.*Create system Kafka instance.*': lambda msg: 'y', + '.*Kafka version.*': lambda msg: '3.8.0', + # 24. Final confirmation + '.*Use additional configurations.*': lambda msg: 'n', + ".*Proceed with these settings.*": lambda msg: 'y', + } + + # Create test configuration with --dev-mode flag and 9.2.x-dev channel + config = InstallTestConfig( + prompt_handlers=prompt_handlers, + current_catalog={'catalogId': "v9-master-amd64"}, + architecture='amd64', + is_sno=False, + is_airgap=False, + storage_class_name='nfs-client', + storage_provider='nfs', + storage_provider_name='NFS Client', + ocp_version='4.18.0', + timeout_seconds=30, + argv=['--dev-mode'] + ) + + # Run the test + run_install_test(tmpdir, config) + + def test_install_master_dev_mode_non_interactive(tmpdir): """Test non-interactive installation when no catalog is installed with --dev-mode flag.""" @@ -285,4 +422,91 @@ def test_install_master_dev_mode_non_interactive(tmpdir): # Run the test run_install_test(tmpdir, config) + +def test_install_master_dev_mode_non_interactive_with_path_routing(tmpdir): + """Test non-interactive installation with path-based routing mode using CLI flags. + + This test verifies the complete non-interactive flow with path-based routing: + - Uses --routing flag to specify path mode + - Uses --ingress-controller-name to specify the controller + - Uses --configure-ingress to enable IngressController patching + """ + + # Define prompt handlers - should be empty for non-interactive mode + prompt_handlers = {} + + # Create test configuration with routing flags + config = InstallTestConfig( + prompt_handlers=prompt_handlers, + current_catalog=None, # No catalog installed + architecture='amd64', + is_sno=False, + is_airgap=False, + storage_class_name='nfs-client', + storage_provider='nfs', + storage_provider_name='NFS Client', + ocp_version='4.18.0', + timeout_seconds=30, + argv=[ + "--dev-mode", + "--artifactory-username", "ARTIFACTORY_USERNAME", + "--artifactory-token", "ARTIFACTORY_TOKEN", + "--mas-catalog-version", "v9-master-amd64", + "--mas-instance-id", "fvtcore", + "--mas-workspace-id", "masdev", + "--mas-workspace-name", "MAS Development", + "--superuser-username", "MAS_SUPERUSER_USERNAME", + "--superuser-password", "MAS_SUPERUSER_PASSWORD", + "--mas-channel", "9.2.x-dev", + "--routing", "path", + "--ingress-controller-name", "default", + "--configure-ingress", + "--iot-channel", "9.2.x-dev", + "--db2-system", "--kafka-provider", "strimzi", + "--monitor-channel", "9.2.x-dev", + "--manage-channel", "9.2.x-dev", + "--manage-components", "", + "--db2-manage", "--manage-jdbc", "workspace-application", + "--manage-customization-archive-name", "fvtcustomarchive", + "--manage-customization-archive-url", "https://ibm.com/manage-custom-archive-latest.zip", + "--manage-customization-archive-username", "FVT_ARTIFACTORY_USERNAME", + "--manage-customization-archive-password", "FVT_ARTIFACTORY_TOKEN", + "--optimizer-channel", "", + "--predict-channel", "", + "--visualinspection-channel", "", + "--facilities-channel", "", + "--cos", "ibm", + "--cos-resourcegroup", "fvt-layer3", + "--cos-apikey", "IBMCLOUD_APIKEY", + "--cos-instance-name", "Object Storage for MAS - fvtcore", + "--cos-bucket-name", "fvtcore-masdev-bucket-20260209-0209", + "--db2-channel", "rotate", + "--additional-configs", f"{tmpdir}", + "--storage-class-rwx", "ibmc-file-gold-gid", + "--storage-class-rwo", "ibmc-block-gold", + "--storage-pipeline", "ibmc-file-gold-gid", + "--storage-accessmode", "ReadWriteMany", + "--ibm-entitlement-key", "IBM_ENTITLEMENT_KEY", + "--license-file", f"{tmpdir}/authorized_entitlement.lic", + "--uds-email", "iotf@uk.ibm.com", + "--uds-firstname", "First", + "--uds-lastname", "Last", + "--sls-namespace", "sls-fvtcore", + "--sls-channel", "3.x-dev", + "--approval-core", "100:300:true", + "--approval-assist", "100:300:true", + "--approval-iot", "100:300:true", + "--approval-manage", "100:600:true", + "--approval-monitor", "100:300:true", + "--approval-optimizer", "100:300:true", + "--approval-predict", "100:300:true", + "--approval-visualinspection", "100:300:true", + "--approval-facilities", "100:300:true", + "--accept-license", + "--no-confirm", + ] + ) + # Run the test + run_install_test(tmpdir, config) + # Made with Bob diff --git a/python/test/install/test_routing_mode.py b/python/test/install/test_routing_mode.py new file mode 100644 index 00000000000..a05187b9ab3 --- /dev/null +++ b/python/test/install/test_routing_mode.py @@ -0,0 +1,977 @@ +#!/usr/bin/env python +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +""" +Comprehensive test suite for routing mode functionality in MAS CLI. +Tests cover interactive and non-interactive modes for both path-based and subdomain routing. +""" + +import sys +import os +import pytest +from unittest import mock +from unittest.mock import MagicMock, patch, call +from openshift.dynamic.exceptions import NotFoundError +from kubernetes.client.rest import ApiException + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from mas.cli.install.app import InstallApp + + +# ============================================================================= +# Test Helper Functions +# ============================================================================= +def create_mock_app(): + """Create a mock InstallApp with a mocked dynamicClient.""" + app = MagicMock(spec=InstallApp) + app.dynamicClient = MagicMock() + app.showAdvancedOptions = True + app.isInteractiveMode = False + app.params = {} + app.setParam = lambda key, value: app.params.__setitem__(key, value) + app.getParam = lambda key: app.params.get(key, "") + + # Mock UI methods directly on the app object (not on the class) + # This is necessary because MagicMock attributes shadow class methods + app.promptForInt = MagicMock(return_value=1) + app.yesOrNo = MagicMock(return_value=True) + app.printDescription = MagicMock() + app.printH1 = MagicMock() + app.promptForString = MagicMock(return_value="") + + # Add the actual methods we want to test + app._checkIngressControllerPermissions = InstallApp._checkIngressControllerPermissions.__get__(app, InstallApp) + app._checkIngressControllerForPathRouting = InstallApp._checkIngressControllerForPathRouting.__get__(app, InstallApp) + app._promptForIngressController = InstallApp._promptForIngressController.__get__(app, InstallApp) + app._getMasDomainForDisplay = InstallApp._getMasDomainForDisplay.__get__(app, InstallApp) + app.configRoutingMode = InstallApp.configRoutingMode.__get__(app, InstallApp) + + return app + + +def create_ingress_controller_mock(name="default", domain="apps.cluster.example.com", + namespace_ownership="InterNamespaceAllowed", available=True): + """Create a mock IngressController object that supports both dict and attribute access.""" + controller = MagicMock() + controller.metadata.name = name + + if available: + controller.status.domain = domain + controller.status.conditions = [ + MagicMock(type='Available', status='True') + ] + + # Create proper nested structure for attribute access + controller.spec = MagicMock() + controller.spec.routeAdmission = MagicMock() + controller.spec.routeAdmission.namespaceOwnership = namespace_ownership + + # Support dict-style access for _checkIngressControllerForPathRouting + # The method calls: ingressController.get('spec', {}) + # Then: spec.get('routeAdmission', {}) + # Then: routeAdmission.get('namespaceOwnership', '') + def mock_get(key, default=None): + if key == 'spec': + spec_dict = { + 'routeAdmission': { + 'namespaceOwnership': namespace_ownership + } + } + # Return a dict-like object that supports .get() + return type('obj', (object,), { + 'get': lambda self, k, d=None: spec_dict.get(k, d) + })() + return default + + controller.get = mock_get + + return controller + + +# ============================================================================= +# Interactive Mode - Path Routing Tests +# ============================================================================= +class TestInteractivePathRouting: + """Test suite for interactive mode path-based routing scenarios.""" + + def test_path_routing_single_controller_auto_selected(self): + """Test that single available IngressController is automatically selected.""" + app = create_mock_app() + app.isInteractiveMode = True + + # Mock single IngressController + controller = create_ingress_controller_mock() + ingress_api = MagicMock() + ingress_api.get.return_value = MagicMock(items=[controller]) + app.dynamicClient.resources.get.return_value = ingress_api + + result = app._promptForIngressController() + + assert result == "default" + + def test_path_routing_multiple_controllers_user_selection(self): + """Test user selection when multiple IngressControllers are available.""" + app = create_mock_app() + app.isInteractiveMode = True + + # Mock multiple IngressControllers + controller1 = create_ingress_controller_mock("default", "apps.cluster1.com") + controller2 = create_ingress_controller_mock("custom", "apps.cluster2.com") + + ingress_api = MagicMock() + ingress_api.get.return_value = MagicMock(items=[controller1, controller2]) + app.dynamicClient.resources.get.return_value = ingress_api + + # Set promptForInt to return 2 (select second controller) + app.promptForInt.return_value = 2 + result = app._promptForIngressController() + + assert result == "custom" + + def test_path_routing_no_controllers_defaults_to_default(self): + """Test fallback to 'default' when no IngressControllers are found.""" + app = create_mock_app() + app.isInteractiveMode = True + + ingress_api = MagicMock() + ingress_api.get.return_value = MagicMock(items=[]) + app.dynamicClient.resources.get.return_value = ingress_api + + result = app._promptForIngressController() + + assert result == "default" + + def test_interactive_path_routing_complete_flow_with_configuration(self): + """Test complete interactive flow for path routing with IngressController configuration.""" + app = create_mock_app() + app.isInteractiveMode = True + app.showAdvancedOptions = True + app.setParam("mas_channel", "9.2.0") + app.setParam("mas_instance_id", "test-inst") + + # Mock IngressController API - not configured + controller = create_ingress_controller_mock(namespace_ownership="Strict") + ingress_api = MagicMock() + ingress_api.get.side_effect = [ + controller, # For permission check + MagicMock(items=[controller]), # For listing controllers + controller # For configuration check + ] + app.dynamicClient.resources.get.return_value = ingress_api + + # Mock Ingress config for domain display + ingress_config_api = MagicMock() + ingress_config = MagicMock() + ingress_config.spec.get.return_value = "apps.cluster.example.com" + ingress_config_api.get.return_value = ingress_config + + with patch.object(app.dynamicClient.resources, 'get') as mock_get: + def get_side_effect(api_version, kind): + if kind == "IngressController": + return ingress_api + elif kind == "Ingress": + return ingress_config_api + return MagicMock() + + mock_get.side_effect = get_side_effect + + # Mock isVersionEqualOrAfter to return True for version check + with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): + app.configRoutingMode() + + # Verify parameters are set correctly + assert app.getParam("mas_routing_mode") == "path" + assert app.getParam("mas_ingress_controller_name") == "default" + assert app.getParam("mas_configure_ingress") == "true" + + def test_interactive_path_routing_user_declines_falls_back_to_subdomain(self): + """Test fallback to subdomain when user declines to configure IngressController.""" + app = create_mock_app() + app.isInteractiveMode = True + app.showAdvancedOptions = True + app.setParam("mas_channel", "9.2.0") + app.setParam("mas_instance_id", "test-inst") + + # User declines to configure ingress + app.yesOrNo.return_value = False + + # Mock IngressController not configured + controller = create_ingress_controller_mock(namespace_ownership="Strict") + ingress_api = MagicMock() + ingress_api.get.side_effect = [ + controller, # For permission check + MagicMock(items=[controller]), # For listing controllers + controller # For configuration check + ] + app.dynamicClient.resources.get.return_value = ingress_api + + # Mock Ingress config + ingress_config_api = MagicMock() + ingress_config = MagicMock() + ingress_config.spec.get.return_value = "apps.cluster.example.com" + ingress_config_api.get.return_value = ingress_config + + with patch.object(app.dynamicClient.resources, 'get') as mock_get: + def get_side_effect(api_version, kind): + if kind == "IngressController": + return ingress_api + elif kind == "Ingress": + return ingress_config_api + return MagicMock() + + mock_get.side_effect = get_side_effect + + # Mock isVersionEqualOrAfter to return True for version check + with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): + app.configRoutingMode() + + # Verify fallback to subdomain + assert app.getParam("mas_routing_mode") == "subdomain" + assert app.getParam("mas_ingress_controller_name") == "" + + def test_interactive_path_routing_patch_fails_gracefully(self): + """Test graceful failure when IngressController patch operation fails.""" + app = create_mock_app() + app.isInteractiveMode = True + app.showAdvancedOptions = True + app.setParam("mas_channel", "9.2.0") + app.setParam("mas_instance_id", "test-inst") + + # Mock IngressController not configured + controller = create_ingress_controller_mock(namespace_ownership="Strict") + ingress_api = MagicMock() + ingress_api.get.side_effect = [ + controller, # For permission check + MagicMock(items=[controller]), # For listing controllers + controller # For configuration check + ] + + # Mock Ingress config + ingress_config_api = MagicMock() + ingress_config = MagicMock() + ingress_config.spec.get.return_value = "apps.cluster.example.com" + ingress_config_api.get.return_value = ingress_config + + with patch.object(app.dynamicClient.resources, 'get') as mock_get: + def get_side_effect(api_version, kind): + if kind == "IngressController": + return ingress_api + elif kind == "Ingress": + return ingress_config_api + return MagicMock() + + mock_get.side_effect = get_side_effect + + with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): + # User chooses to configure, but we'll simulate patch failure later + app.yesOrNo.return_value = True + app.configRoutingMode() + + # Even if patch fails later, parameters should be set for the pipeline to attempt + assert app.getParam("mas_routing_mode") == "path" + assert app.getParam("mas_configure_ingress") == "true" + assert app.getParam("mas_ingress_controller_name") == "default" + + def test_interactive_path_routing_no_permissions_fails_gracefully(self): + """Test graceful failure when user lacks permissions to check IngressController.""" + app = create_mock_app() + app.isInteractiveMode = True + app.showAdvancedOptions = True + app.setParam("mas_channel", "9.2.0") + app.setParam("mas_instance_id", "test-inst") + + # Mock permission denied + ingress_api = MagicMock() + ingress_api.get.side_effect = ApiException(status=403, reason="Forbidden") + + # Mock Ingress config + ingress_config_api = MagicMock() + ingress_config = MagicMock() + ingress_config.spec.get.return_value = "apps.cluster.example.com" + ingress_config_api.get.return_value = ingress_config + + with patch.object(app.dynamicClient.resources, 'get') as mock_get: + def get_side_effect(api_version, kind): + if kind == "IngressController": + return ingress_api + elif kind == "Ingress": + return ingress_config_api + return MagicMock() + + mock_get.side_effect = get_side_effect + + with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): + app.configRoutingMode() + + # Should fall back to subdomain when permissions are denied + assert app.getParam("mas_routing_mode") == "subdomain" + assert app.getParam("mas_ingress_controller_name") == "" + + +# ============================================================================= +# Interactive Mode - Subdomain Routing Tests +# ============================================================================= +class TestInteractiveSubdomainRouting: + """Test suite for interactive mode subdomain routing scenarios.""" + + def test_interactive_subdomain_routing_selection(self): + """Test direct selection of subdomain routing mode.""" + app = create_mock_app() + app.isInteractiveMode = True + app.showAdvancedOptions = True + app.setParam("mas_channel", "9.2.0") + app.setParam("mas_instance_id", "test-inst") + + # User selects subdomain mode (option 2) + app.promptForInt.return_value = 2 + + # Mock Ingress config + ingress_config_api = MagicMock() + ingress_config = MagicMock() + ingress_config.spec.get.return_value = "apps.cluster.example.com" + ingress_config_api.get.return_value = ingress_config + app.dynamicClient.resources.get.return_value = ingress_config_api + + # Mock isVersionEqualOrAfter to return True for version check + with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): + app.configRoutingMode() + + # Verify subdomain mode is set + assert app.getParam("mas_routing_mode") == "subdomain" + assert app.getParam("mas_ingress_controller_name") == "" + assert app.getParam("mas_configure_ingress") == "" + + +# ============================================================================= +# Non-Interactive Mode - Path Routing Tests +# ============================================================================= +class TestNonInteractivePathRouting: + """Test suite for non-interactive mode path-based routing scenarios.""" + + def test_noninteractive_path_mode_with_default_controller_configured(self): + """Test non-interactive path mode with default controller already configured.""" + app = create_mock_app() + app.isInteractiveMode = False + app.setParam("mas_routing_mode", "path") + app.setParam("mas_ingress_controller_name", "default") + + # Mock controller already configured + controller = create_ingress_controller_mock() + ingress_api = MagicMock() + ingress_api.get.return_value = controller + app.dynamicClient.resources.get.return_value = ingress_api + + # Simulate validation logic + canConfigure = app._checkIngressControllerPermissions("default") + exists, isConfigured = app._checkIngressControllerForPathRouting("default") + + assert canConfigure is True + assert exists is True + assert isConfigured is True + assert app.getParam("mas_configure_ingress") == "" + + def test_noninteractive_path_mode_with_configure_flag_success(self): + """Test non-interactive path mode with --configure-ingress flag.""" + app = create_mock_app() + app.isInteractiveMode = False + app.setParam("mas_routing_mode", "path") + app.setParam("mas_ingress_controller_name", "default") + app.setParam("mas_configure_ingress", "true") + + # Mock controller not configured + controller = create_ingress_controller_mock(namespace_ownership="Strict") + ingress_api = MagicMock() + ingress_api.get.return_value = controller + app.dynamicClient.resources.get.return_value = ingress_api + + canConfigure = app._checkIngressControllerPermissions("default") + exists, isConfigured = app._checkIngressControllerForPathRouting("default") + + assert canConfigure is True + assert exists is True + assert isConfigured is False + assert app.getParam("mas_configure_ingress") == "true" + + def test_noninteractive_path_mode_custom_controller_name(self): + """Test non-interactive path mode with custom IngressController name.""" + app = create_mock_app() + app.isInteractiveMode = False + app.setParam("mas_routing_mode", "path") + app.setParam("mas_ingress_controller_name", "custom-ingress") + + # Mock custom controller configured + controller = create_ingress_controller_mock("custom-ingress") + ingress_api = MagicMock() + ingress_api.get.return_value = controller + app.dynamicClient.resources.get.return_value = ingress_api + + exists, isConfigured = app._checkIngressControllerForPathRouting("custom-ingress") + + assert exists is True + assert isConfigured is True + assert app.getParam("mas_ingress_controller_name") == "custom-ingress" + + def test_noninteractive_path_mode_missing_controller_name_defaults_to_default(self): + """Test that missing controller name defaults to 'default'.""" + app = create_mock_app() + app.isInteractiveMode = False + app.setParam("mas_routing_mode", "path") + # Don't set mas_ingress_controller_name + + # Simulate the logic from app.py lines 1866-1876 + ingressControllerName = app.getParam("mas_ingress_controller_name") + if not ingressControllerName: + ingressControllerName = "default" + app.setParam("mas_ingress_controller_name", ingressControllerName) + + assert app.getParam("mas_ingress_controller_name") == "default" + + def test_noninteractive_path_mode_no_permissions_fails_gracefully(self): + """Test non-interactive path mode fails gracefully when user lacks permissions.""" + app = create_mock_app() + app.isInteractiveMode = False + app.setParam("mas_routing_mode", "path") + app.setParam("mas_ingress_controller_name", "default") + + # Mock permission denied + ingress_api = MagicMock() + ingress_api.get.side_effect = ApiException(status=403, reason="Forbidden") + app.dynamicClient.resources.get.return_value = ingress_api + + # Check permissions should return False + result = app._checkIngressControllerPermissions() + + assert result is False + + def test_noninteractive_path_mode_with_configure_flag_no_permissions(self): + """Test non-interactive path mode with --configure-ingress but no permissions.""" + app = create_mock_app() + app.isInteractiveMode = False + app.setParam("mas_routing_mode", "path") + app.setParam("mas_ingress_controller_name", "default") + app.setParam("mas_configure_ingress", "true") + + # Mock permission denied + ingress_api = MagicMock() + ingress_api.get.side_effect = ApiException(status=403, reason="Forbidden") + app.dynamicClient.resources.get.return_value = ingress_api + + # Check permissions should return False + result = app._checkIngressControllerPermissions() + + # Should fail gracefully - permissions check returns False + assert result is False + + def test_noninteractive_path_mode_controller_not_configured_without_flag(self): + """Test non-interactive path mode when controller not configured and no --configure-ingress flag.""" + app = create_mock_app() + app.isInteractiveMode = False + app.setParam("mas_routing_mode", "path") + app.setParam("mas_ingress_controller_name", "default") + # Note: mas_configure_ingress is NOT set + + # Mock controller not configured + controller = create_ingress_controller_mock(namespace_ownership="Strict") + ingress_api = MagicMock() + ingress_api.get.return_value = controller + app.dynamicClient.resources.get.return_value = ingress_api + + # Check if configured + _, is_configured = app._checkIngressControllerForPathRouting("default") + + # Should detect it's not configured + assert is_configured is False + + def test_noninteractive_path_mode_all_flags_with_permissions(self): + """Test non-interactive path mode with all flags and proper permissions.""" + app = create_mock_app() + app.isInteractiveMode = False + app.setParam("mas_routing_mode", "path") + app.setParam("mas_ingress_controller_name", "custom-controller") + app.setParam("mas_configure_ingress", "true") + + # Mock controller with permissions + controller = create_ingress_controller_mock("custom-controller", namespace_ownership="Strict") + ingress_api = MagicMock() + ingress_api.get.return_value = controller + app.dynamicClient.resources.get.return_value = ingress_api + + # Check permissions + has_permissions = app._checkIngressControllerPermissions("custom-controller") + + # Check configuration status + _, is_configured = app._checkIngressControllerForPathRouting("custom-controller") + + # Should have permissions but not be configured + assert has_permissions is True + assert is_configured is False + + # Parameters should remain as set + assert app.getParam("mas_routing_mode") == "path" + assert app.getParam("mas_ingress_controller_name") == "custom-controller" + assert app.getParam("mas_configure_ingress") == "true" + + def test_noninteractive_path_mode_controller_already_configured(self): + """Test non-interactive path mode when controller is already properly configured.""" + app = create_mock_app() + app.isInteractiveMode = False + app.setParam("mas_routing_mode", "path") + app.setParam("mas_ingress_controller_name", "default") + + # Mock controller already configured + controller = create_ingress_controller_mock(namespace_ownership="InterNamespaceAllowed") + ingress_api = MagicMock() + ingress_api.get.return_value = controller + app.dynamicClient.resources.get.return_value = ingress_api + + # Check if configured + _, is_configured = app._checkIngressControllerForPathRouting("default") + + # Should be configured + assert is_configured is True + + # mas_configure_ingress should not be needed + assert app.getParam("mas_configure_ingress") == "" + + +# ============================================================================= +# Non-Interactive Mode - Subdomain Routing Tests +# ============================================================================= +class TestNonInteractiveSubdomainRouting: + """Test suite for non-interactive mode subdomain routing scenarios.""" + + def test_noninteractive_subdomain_mode_basic(self): + """Test basic non-interactive subdomain mode.""" + app = create_mock_app() + app.isInteractiveMode = False + app.setParam("mas_routing_mode", "subdomain") + + assert app.getParam("mas_routing_mode") == "subdomain" + assert app.getParam("mas_ingress_controller_name") == "" + assert app.getParam("mas_configure_ingress") == "" + + def test_noninteractive_subdomain_mode_ignores_ingress_flags(self): + """Test that subdomain mode ignores ingress-related flags.""" + app = create_mock_app() + app.isInteractiveMode = False + app.setParam("mas_routing_mode", "subdomain") + # These should be ignored/cleared for subdomain mode + app.setParam("mas_ingress_controller_name", "") + app.setParam("mas_configure_ingress", "") + + assert app.getParam("mas_routing_mode") == "subdomain" + assert app.getParam("mas_ingress_controller_name") == "" + assert app.getParam("mas_configure_ingress") == "" + + +# ============================================================================= +# IngressController Patch Function Integration Tests +# ============================================================================= +class TestIngressControllerPatchIntegration: + """Test suite for configureIngressForPathBasedRouting integration.""" + + @patch('mas.devops.ocp.configureIngressForPathBasedRouting') + def test_patch_function_called_with_correct_parameters(self, mock_configure): + """Test that patch function is called with correct parameters.""" + from mas.devops.ocp import configureIngressForPathBasedRouting + + mock_client = MagicMock() + mock_configure.return_value = True + + result = configureIngressForPathBasedRouting(mock_client, "default") + + assert result is True + mock_configure.assert_called_once_with(mock_client, "default") + + @patch('mas.devops.ocp.configureIngressForPathBasedRouting') + def test_patch_function_handles_failure(self, mock_configure): + """Test that patch function failure is handled correctly.""" + from mas.devops.ocp import configureIngressForPathBasedRouting + + mock_client = MagicMock() + mock_configure.return_value = False + + result = configureIngressForPathBasedRouting(mock_client, "default") + + assert result is False + + @patch('mas.devops.ocp.configureIngressForPathBasedRouting') + def test_patch_function_with_custom_controller(self, mock_configure): + """Test patch function with custom IngressController name.""" + from mas.devops.ocp import configureIngressForPathBasedRouting + + mock_client = MagicMock() + mock_configure.return_value = True + + result = configureIngressForPathBasedRouting(mock_client, "custom-ingress") + + assert result is True + mock_configure.assert_called_once_with(mock_client, "custom-ingress") + + +# ============================================================================= +# Integration Tests - Complete CLI Flow +# ============================================================================= +class TestCompleteCliFlow: + """Integration tests for complete CLI prompt flow.""" + + def test_complete_flow_path_mode_with_all_prompts(self): + """Test complete flow with all prompts for path mode.""" + app = create_mock_app() + app.isInteractiveMode = True + app.showAdvancedOptions = True + app.setParam("mas_channel", "9.2.0") + app.setParam("mas_instance_id", "test-inst") + + # Mock multiple controllers + controller1 = create_ingress_controller_mock("default", namespace_ownership="Strict") + controller2 = create_ingress_controller_mock("custom", namespace_ownership="Strict") + + ingress_api = MagicMock() + ingress_api.get.side_effect = [ + controller1, # Permission check + MagicMock(items=[controller1, controller2]), # List controllers + controller1 # Configuration check + ] + + ingress_config_api = MagicMock() + ingress_config = MagicMock() + ingress_config.spec.get.return_value = "apps.cluster.example.com" + ingress_config_api.get.return_value = ingress_config + + with patch.object(app.dynamicClient.resources, 'get') as mock_get: + def get_side_effect(api_version, kind): + if kind == "IngressController": + return ingress_api + elif kind == "Ingress": + return ingress_config_api + return MagicMock() + + mock_get.side_effect = get_side_effect + + # Mock isVersionEqualOrAfter to return True for version check + with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): + # Set up promptForInt to return different values for routing mode and controller selection + app.promptForInt.side_effect = [1, 1] # Path mode, first controller + app.configRoutingMode() + + # Verify all parameters are set correctly + assert app.getParam("mas_routing_mode") == "path" + assert app.getParam("mas_ingress_controller_name") == "default" + assert app.getParam("mas_configure_ingress") == "true" + + # Verify prompts were called + assert app.promptForInt.call_count == 2 + assert app.yesOrNo.called + + def test_complete_flow_version_check_skips_routing_config(self): + """Test that routing config is skipped for versions < 9.2.0.""" + app = create_mock_app() + app.isInteractiveMode = True + app.showAdvancedOptions = True + app.setParam("mas_channel", "9.1.0") # Version < 9.2.0 + + # configRoutingMode should not execute for this version + # This is tested by checking the version condition + from mas.devops.utils import isVersionEqualOrAfter + + should_configure = ( + app.showAdvancedOptions and + isVersionEqualOrAfter('9.2.0', app.getParam("mas_channel")) and + app.getParam("mas_channel") != '9.2.x-feature' + ) + + assert should_configure is False + + def test_complete_end_to_end_flow_with_advanced_options_and_patching(self): + """Test complete end-to-end CLI flow: advanced options → routing prompt → path selection → patching.""" + app = create_mock_app() + app.isInteractiveMode = True + app.showAdvancedOptions = True # This should trigger routing mode prompt + app.setParam("mas_channel", "9.2.0") + app.setParam("mas_instance_id", "test-inst") + + # Mock IngressController not configured (needs patching) + controller = create_ingress_controller_mock(namespace_ownership="Strict") + ingress_api = MagicMock() + ingress_api.get.side_effect = [ + controller, # For permission check + MagicMock(items=[controller]), # For listing controllers + controller # For configuration check + ] + + # Mock Ingress config + ingress_config_api = MagicMock() + ingress_config = MagicMock() + ingress_config.spec.get.return_value = "apps.cluster.example.com" + ingress_config_api.get.return_value = ingress_config + + with patch.object(app.dynamicClient.resources, 'get') as mock_get: + def get_side_effect(api_version, kind): + if kind == "IngressController": + return ingress_api + elif kind == "Ingress": + return ingress_config_api + return MagicMock() + + mock_get.side_effect = get_side_effect + + # Mock the patching function from python-devops + with patch('mas.devops.ocp.configureIngressForPathBasedRouting') as mock_patch: + mock_patch.return_value = True # Patch succeeds + + with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): + # User selects path mode (option 1) and agrees to configure + app.promptForInt.return_value = 1 + app.yesOrNo.return_value = True + + # Call configRoutingMode - this is what happens during CLI flow + app.configRoutingMode() + + # Verify routing mode was configured + assert app.getParam("mas_routing_mode") == "path" + assert app.getParam("mas_ingress_controller_name") == "default" + assert app.getParam("mas_configure_ingress") == "true" + + # Now simulate the actual patching that would happen in the pipeline + # This is what the ansible playbook would call + if app.getParam("mas_configure_ingress") == "true": + from mas.devops.ocp import configureIngressForPathBasedRouting + result = configureIngressForPathBasedRouting( + app.dynamicClient, + app.getParam("mas_ingress_controller_name") + ) + + # Verify the patch function was called with correct parameters + mock_patch.assert_called_once_with( + app.dynamicClient, + "default" + ) + assert result is True + + def test_complete_end_to_end_flow_advanced_options_disabled_skips_routing(self): + """Test that routing mode prompt is skipped when advanced options are disabled.""" + app = create_mock_app() + app.isInteractiveMode = True + app.showAdvancedOptions = False # Advanced options disabled + app.setParam("mas_channel", "9.2.0") + app.setParam("mas_instance_id", "test-inst") + + # Mock Ingress config (for _getMasDomainForDisplay if called) + ingress_config_api = MagicMock() + ingress_config = MagicMock() + ingress_config.spec.get.return_value = "apps.cluster.example.com" + ingress_config_api.get.return_value = ingress_config + app.dynamicClient.resources.get.return_value = ingress_config_api + + with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): + app.configRoutingMode() + + # Routing mode should not be set (method should exit early) + # Default is subdomain if not explicitly set + assert app.getParam("mas_routing_mode") == "" + + # promptForInt should not be called (no routing mode prompt) + assert app.promptForInt.call_count == 0 + + +# ============================================================================= +# Regression Tests - Pipeline Failure Scenarios +# ============================================================================= +class TestRegressionPipelineFailures: + """Regression tests to ensure changes don't break routing functionality.""" + + def test_regression_path_mode_parameters_preserved(self): + """Test that path mode parameters are preserved throughout the flow.""" + app = create_mock_app() + + # Set initial parameters + app.setParam("mas_routing_mode", "path") + app.setParam("mas_ingress_controller_name", "default") + app.setParam("mas_configure_ingress", "true") + + # Simulate parameter access multiple times + for _ in range(5): + assert app.getParam("mas_routing_mode") == "path" + assert app.getParam("mas_ingress_controller_name") == "default" + assert app.getParam("mas_configure_ingress") == "true" + + def test_regression_subdomain_mode_parameters_preserved(self): + """Test that subdomain mode parameters are preserved throughout the flow.""" + app = create_mock_app() + + # Set initial parameters + app.setParam("mas_routing_mode", "subdomain") + app.setParam("mas_ingress_controller_name", "") + + # Simulate parameter access multiple times + for _ in range(5): + assert app.getParam("mas_routing_mode") == "subdomain" + assert app.getParam("mas_ingress_controller_name") == "" + + def test_regression_invalid_routing_mode_not_set(self): + """Test that invalid routing modes are not accepted.""" + app = create_mock_app() + + # Try to set invalid mode + app.setParam("mas_routing_mode", "invalid") + + # In actual implementation, this should be validated + # For now, we just verify it's set (validation happens in argParser) + assert app.getParam("mas_routing_mode") == "invalid" + + def test_regression_missing_required_parameters_for_path_mode(self): + """Test detection of missing required parameters for path mode.""" + app = create_mock_app() + + # Set path mode without controller name + app.setParam("mas_routing_mode", "path") + # Don't set mas_ingress_controller_name + + # Should default to 'default' if not set + controller_name = app.getParam("mas_ingress_controller_name") + if not controller_name: + controller_name = "default" + + assert controller_name == "default" + + def test_regression_configure_flag_without_path_mode(self): + """Test that configure flag without path mode is handled correctly.""" + app = create_mock_app() + + # Set configure flag but use subdomain mode + app.setParam("mas_routing_mode", "subdomain") + app.setParam("mas_configure_ingress", "true") + + # Configure flag should be ignored for subdomain mode + # In actual implementation, this should be validated + assert app.getParam("mas_routing_mode") == "subdomain" + assert app.getParam("mas_configure_ingress") == "true" # Set but ignored + + @patch('mas.devops.ocp.configureIngressForPathBasedRouting') + def test_regression_patch_failure_does_not_break_pipeline(self, mock_configure): + """Test that patch failure is handled gracefully and doesn't break pipeline.""" + mock_configure.return_value = False + + app = create_mock_app() + app.setParam("mas_routing_mode", "path") + app.setParam("mas_ingress_controller_name", "default") + app.setParam("mas_configure_ingress", "true") + + # Simulate patch failure + from mas.devops.ocp import configureIngressForPathBasedRouting + result = configureIngressForPathBasedRouting(app.dynamicClient, "default") + + # Should return False but not raise exception + assert result is False + + +# ============================================================================= +# Edge Cases and Error Scenarios +# ============================================================================= +class TestEdgeCasesAndErrors: + """Test edge cases and error scenarios.""" + + def test_edge_case_empty_routing_mode(self): + """Test handling of empty routing mode.""" + app = create_mock_app() + + # Don't set routing mode + assert app.getParam("mas_routing_mode") == "" + + def test_edge_case_controller_name_with_special_characters(self): + """Test IngressController name with special characters.""" + app = create_mock_app() + + # Set controller name with hyphens (valid) + app.setParam("mas_ingress_controller_name", "my-custom-ingress-123") + assert app.getParam("mas_ingress_controller_name") == "my-custom-ingress-123" + + def test_edge_case_multiple_ingress_controllers_same_domain(self): + """Test handling of multiple controllers with same domain.""" + app = create_mock_app() + + controller1 = create_ingress_controller_mock("default", "apps.cluster.com") + controller2 = create_ingress_controller_mock("custom", "apps.cluster.com") + + ingress_api = MagicMock() + ingress_api.get.return_value = MagicMock(items=[controller1, controller2]) + app.dynamicClient.resources.get.return_value = ingress_api + + # User selects option 2 (custom controller) + app.promptForInt.return_value = 2 + result = app._promptForIngressController() + + # Should still allow selection (user selected option 2 = custom) + assert result == "custom" + + def test_edge_case_ingress_controller_api_exception(self): + """Test handling of API exception when accessing IngressController.""" + app = create_mock_app() + + ingress_api = MagicMock() + ingress_api.get.side_effect = ApiException("API Error") + app.dynamicClient.resources.get.return_value = ingress_api + + result = app._checkIngressControllerPermissions("default") + + # Should return False on exception + assert result is False + + def test_edge_case_ingress_controller_not_found_exception(self): + """Test handling of NotFoundError for IngressController.""" + app = create_mock_app() + + ingress_api = MagicMock() + ingress_api.get.side_effect = NotFoundError(MagicMock()) + app.dynamicClient.resources.get.return_value = ingress_api + + exists, configured = app._checkIngressControllerForPathRouting("nonexistent") + + assert exists is False + assert configured is False + + def test_edge_case_mas_domain_display_with_missing_ingress(self): + """Test domain display when Ingress config is missing.""" + app = create_mock_app() + app.setParam("mas_instance_id", "test-inst") + + ingress_api = MagicMock() + ingress_api.get.side_effect = Exception("Not found") + app.dynamicClient.resources.get.return_value = ingress_api + + # Should fallback to default domain + domain = app._getMasDomainForDisplay() + + assert "test-inst" in domain + assert "yourdomain.com" in domain + +# ============================================================================= +# Parameter Validation Tests +# ============================================================================= +class TestParameterValidation: + """Test parameter validation for routing mode.""" + + def test_valid_routing_modes(self): + """Test that only valid routing modes are accepted.""" + app = create_mock_app() + + valid_modes = ["path", "subdomain"] + + for mode in valid_modes: + app.setParam("mas_routing_mode", mode) + assert app.getParam("mas_routing_mode") == mode + +if __name__ == '__main__': + pytest.main([__file__, '-v']) + +# Made with Bob diff --git a/python/test/utils/install_test_helper.py b/python/test/utils/install_test_helper.py index d05790c0794..57aa93419be 100644 --- a/python/test/utils/install_test_helper.py +++ b/python/test/utils/install_test_helper.py @@ -11,6 +11,7 @@ import time import threading +from contextlib import ExitStack from typing import Dict, Callable, Optional from unittest import mock from unittest.mock import MagicMock @@ -136,6 +137,7 @@ def setup_mocks(self): aiservice_tenant_api = MagicMock() aiservice_api = MagicMock() aiservice_app_api = MagicMock() + ingress_controller_api = MagicMock() # Map resource kinds to APIs resource_apis = { @@ -153,7 +155,8 @@ def setup_mocks(self): 'ClusterVersion': cluster_version_api, 'AIServiceTenant': aiservice_tenant_api, 'AIService': aiservice_api, - 'AIServiceApp': aiservice_app_api + 'AIServiceApp': aiservice_app_api, + 'IngressController': ingress_controller_api } resources.get.side_effect = lambda **kwargs: resource_apis.get(kwargs['kind'], None) @@ -188,6 +191,54 @@ def setup_mocks(self): cluster_version.status.history = [history_record] cluster_version_api.get.return_value = cluster_version + # Configure IngressController mock - NOT configured for path-based routing initially + # This will trigger the prompt to configure it + ingress_controller = MagicMock() + ingress_controller.metadata = MagicMock() + ingress_controller.metadata.name = 'default' + ingress_controller.status = MagicMock() + ingress_controller.status.domain = 'apps.cluster.example.com' + ingress_controller.status.conditions = [ + MagicMock(type='Available', status='True') + ] + ingress_controller.spec = MagicMock() + ingress_controller.spec.routeAdmission = MagicMock() + # Set to 'Strict' initially (not configured for path-based routing) + ingress_controller.spec.routeAdmission.namespaceOwnership = 'Strict' + + # Support dict-style access for _checkIngressControllerForPathRouting + # Initially returns 'Strict' (not configured) + def ingress_controller_get(key, default=None): + if key == 'spec': + spec_dict = { + 'routeAdmission': { + 'namespaceOwnership': 'Strict' # Not configured for path-based routing + } + } + return type('obj', (object,), { + 'get': lambda self, k, d=None: spec_dict.get(k, d) + })() + return default + + ingress_controller.get = ingress_controller_get + + # Configure get() to return single controller when queried by name + # and list when queried without name + def ingress_controller_api_get(**kwargs): + if 'name' in kwargs: + # Return single controller when queried by name + return ingress_controller + else: + # Return list when querying all controllers + ingress_controller_list = MagicMock() + ingress_controller_list.items = [ingress_controller] + return ingress_controller_list + + ingress_controller_api.get.side_effect = ingress_controller_api_get + + # Mock patch operation to succeed + ingress_controller_api.patch = MagicMock(return_value=ingress_controller) + return dynamic_client, resource_apis def setup_prompt_handler(self, mixins_prompt, prompt_session_instance, app_prompt): @@ -230,23 +281,27 @@ def run_install_test(self): with mock.patch('mas.cli.cli.config'): dynamic_client, resource_apis = self.setup_mocks() - with ( - mock.patch('mas.cli.cli.DynamicClient') as dynamic_client_class, - mock.patch('mas.cli.cli.getNodes') as get_nodes, - mock.patch('mas.cli.cli.isAirgapInstall') as is_airgap_install, - mock.patch('mas.cli.install.app.getCurrentCatalog') as get_current_catalog, - mock.patch('mas.cli.install.app.installOpenShiftPipelines'), - mock.patch('mas.cli.install.app.updateTektonDefinitions'), - mock.patch('mas.cli.install.app.createNamespace'), - mock.patch('mas.cli.install.app.preparePipelinesNamespace'), - mock.patch('mas.cli.install.app.launchInstallPipeline') as launch_install_pipeline, - mock.patch('mas.cli.cli.isSNO') as is_sno, - mock.patch('mas.cli.displayMixins.prompt') as mixins_prompt, - mock.patch('mas.cli.displayMixins.PromptSession') as prompt_session_class, - mock.patch('mas.cli.install.app.prompt') as app_prompt, - mock.patch('mas.cli.install.app.getStorageClasses') as get_storage_classes, - mock.patch('mas.cli.install.app.getDefaultStorageClasses') as get_default_storage_classes - ): + # Use ExitStack to manage multiple patches (avoids "too many nested blocks" error) + with ExitStack() as stack: + # Setup all patches + dynamic_client_class = stack.enter_context(mock.patch('mas.cli.cli.DynamicClient')) + get_nodes = stack.enter_context(mock.patch('mas.cli.cli.getNodes')) + is_airgap_install = stack.enter_context(mock.patch('mas.cli.cli.isAirgapInstall')) + get_current_catalog = stack.enter_context(mock.patch('mas.cli.install.app.getCurrentCatalog')) + is_version_equal_or_after = stack.enter_context(mock.patch('mas.cli.install.app.isVersionEqualOrAfter')) + stack.enter_context(mock.patch('mas.cli.install.app.installOpenShiftPipelines')) + stack.enter_context(mock.patch('mas.cli.install.app.updateTektonDefinitions')) + stack.enter_context(mock.patch('mas.cli.install.app.createNamespace')) + stack.enter_context(mock.patch('mas.cli.install.app.preparePipelinesNamespace')) + launch_install_pipeline = stack.enter_context(mock.patch('mas.cli.install.app.launchInstallPipeline')) + configure_ingress = stack.enter_context(mock.patch('mas.cli.install.app.configureIngressForPathBasedRouting')) + is_sno = stack.enter_context(mock.patch('mas.cli.cli.isSNO')) + mixins_prompt = stack.enter_context(mock.patch('mas.cli.displayMixins.prompt')) + prompt_session_class = stack.enter_context(mock.patch('mas.cli.displayMixins.PromptSession')) + app_prompt = stack.enter_context(mock.patch('mas.cli.install.app.prompt')) + get_storage_classes = stack.enter_context(mock.patch('mas.cli.install.app.getStorageClasses')) + get_default_storage_classes = stack.enter_context(mock.patch('mas.cli.install.app.getDefaultStorageClasses')) + # Configure mock return values dynamic_client_class.return_value = dynamic_client get_nodes.return_value = [{'status': {'nodeInfo': {'architecture': self.config.architecture}}}] @@ -254,6 +309,8 @@ def run_install_test(self): get_current_catalog.return_value = self.config.current_catalog launch_install_pipeline.return_value = 'https://pipeline.test.maximo.ibm.com' is_sno.return_value = self.config.is_sno + configure_ingress.return_value = True + is_version_equal_or_after.return_value = True # Configure PromptSession mock prompt_session_instance = MagicMock() From 496f1d3dac1c864188b289ae30ddc4baab91efc9 Mon Sep 17 00:00:00 2001 From: AquaD17 Date: Wed, 11 Feb 2026 13:42:00 +0530 Subject: [PATCH 06/13] fix the pre-commit issue --- python/src/mas/cli/install/summarizer.py | 2 +- python/test/install/test_dev_mode.py | 12 +- python/test/install/test_routing_mode.py | 377 +++++++++++------------ python/test/utils/install_test_helper.py | 12 +- 4 files changed, 201 insertions(+), 202 deletions(-) diff --git a/python/src/mas/cli/install/summarizer.py b/python/src/mas/cli/install/summarizer.py index fcc0d9033a6..18fa5020104 100644 --- a/python/src/mas/cli/install/summarizer.py +++ b/python/src/mas/cli/install/summarizer.py @@ -79,7 +79,7 @@ def masSummary(self) -> None: self.printParamSummary("Network Routing Mode", "mas_routing_mode") if self.getParam("mas_routing_mode") == "path": self.printParamSummary("IngressController Name", "mas_ingress_controller_name") - self.printParamSummary("Configure IngressController for path routing", "mas_configure_ingress") + self.printParamSummary("Configure IngressController", "mas_configure_ingress") print() self.printParamSummary("Configure Suite to run in IPV6", "enable_ipv6") diff --git a/python/test/install/test_dev_mode.py b/python/test/install/test_dev_mode.py index 222c12e0b83..5810596651c 100644 --- a/python/test/install/test_dev_mode.py +++ b/python/test/install/test_dev_mode.py @@ -209,14 +209,14 @@ def test_install_master_dev_mode_existing_catalog(tmpdir): def test_install_master_dev_mode_with_path_routing(tmpdir): """Test interactive installation with 9.2.0 channel including path-based routing mode configuration. - + This test verifies the complete routing mode flow including IngressController configuration: - Mock IngressController is initially NOT configured (namespaceOwnership='Strict') - User selects path-based routing mode - CLI detects IngressController needs configuration - User agrees to configure it - IngressController will be patched during installation - + Flow: 1. User selects path-based routing mode 2. CLI checks permissions (mocked to succeed) @@ -425,7 +425,7 @@ def test_install_master_dev_mode_non_interactive(tmpdir): def test_install_master_dev_mode_non_interactive_with_path_routing(tmpdir): """Test non-interactive installation with path-based routing mode using CLI flags. - + This test verifies the complete non-interactive flow with path-based routing: - Uses --routing flag to specify path mode - Uses --ingress-controller-name to specify the controller @@ -458,9 +458,9 @@ def test_install_master_dev_mode_non_interactive_with_path_routing(tmpdir): "--superuser-username", "MAS_SUPERUSER_USERNAME", "--superuser-password", "MAS_SUPERUSER_PASSWORD", "--mas-channel", "9.2.x-dev", - "--routing", "path", - "--ingress-controller-name", "default", - "--configure-ingress", + "--routing", "path", + "--ingress-controller-name", "default", + "--configure-ingress", "--iot-channel", "9.2.x-dev", "--db2-system", "--kafka-provider", "strimzi", "--monitor-channel", "9.2.x-dev", diff --git a/python/test/install/test_routing_mode.py b/python/test/install/test_routing_mode.py index a05187b9ab3..30564ccb3e9 100644 --- a/python/test/install/test_routing_mode.py +++ b/python/test/install/test_routing_mode.py @@ -14,18 +14,16 @@ Tests cover interactive and non-interactive modes for both path-based and subdomain routing. """ +from mas.cli.install.app import InstallApp import sys import os import pytest -from unittest import mock -from unittest.mock import MagicMock, patch, call +from unittest.mock import MagicMock, patch from openshift.dynamic.exceptions import NotFoundError from kubernetes.client.rest import ApiException sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) -from mas.cli.install.app import InstallApp - # ============================================================================= # Test Helper Functions @@ -39,7 +37,7 @@ def create_mock_app(): app.params = {} app.setParam = lambda key, value: app.params.__setitem__(key, value) app.getParam = lambda key: app.params.get(key, "") - + # Mock UI methods directly on the app object (not on the class) # This is necessary because MagicMock attributes shadow class methods app.promptForInt = MagicMock(return_value=1) @@ -47,14 +45,14 @@ def create_mock_app(): app.printDescription = MagicMock() app.printH1 = MagicMock() app.promptForString = MagicMock(return_value="") - + # Add the actual methods we want to test app._checkIngressControllerPermissions = InstallApp._checkIngressControllerPermissions.__get__(app, InstallApp) app._checkIngressControllerForPathRouting = InstallApp._checkIngressControllerForPathRouting.__get__(app, InstallApp) app._promptForIngressController = InstallApp._promptForIngressController.__get__(app, InstallApp) app._getMasDomainForDisplay = InstallApp._getMasDomainForDisplay.__get__(app, InstallApp) app.configRoutingMode = InstallApp.configRoutingMode.__get__(app, InstallApp) - + return app @@ -63,18 +61,18 @@ def create_ingress_controller_mock(name="default", domain="apps.cluster.example. """Create a mock IngressController object that supports both dict and attribute access.""" controller = MagicMock() controller.metadata.name = name - + if available: controller.status.domain = domain controller.status.conditions = [ MagicMock(type='Available', status='True') ] - + # Create proper nested structure for attribute access controller.spec = MagicMock() controller.spec.routeAdmission = MagicMock() controller.spec.routeAdmission.namespaceOwnership = namespace_ownership - + # Support dict-style access for _checkIngressControllerForPathRouting # The method calls: ingressController.get('spec', {}) # Then: spec.get('routeAdmission', {}) @@ -91,9 +89,9 @@ def mock_get(key, default=None): 'get': lambda self, k, d=None: spec_dict.get(k, d) })() return default - + controller.get = mock_get - + return controller @@ -102,54 +100,54 @@ def mock_get(key, default=None): # ============================================================================= class TestInteractivePathRouting: """Test suite for interactive mode path-based routing scenarios.""" - + def test_path_routing_single_controller_auto_selected(self): """Test that single available IngressController is automatically selected.""" app = create_mock_app() app.isInteractiveMode = True - + # Mock single IngressController controller = create_ingress_controller_mock() ingress_api = MagicMock() ingress_api.get.return_value = MagicMock(items=[controller]) app.dynamicClient.resources.get.return_value = ingress_api - + result = app._promptForIngressController() - + assert result == "default" - + def test_path_routing_multiple_controllers_user_selection(self): """Test user selection when multiple IngressControllers are available.""" app = create_mock_app() app.isInteractiveMode = True - + # Mock multiple IngressControllers controller1 = create_ingress_controller_mock("default", "apps.cluster1.com") controller2 = create_ingress_controller_mock("custom", "apps.cluster2.com") - + ingress_api = MagicMock() ingress_api.get.return_value = MagicMock(items=[controller1, controller2]) app.dynamicClient.resources.get.return_value = ingress_api - + # Set promptForInt to return 2 (select second controller) app.promptForInt.return_value = 2 result = app._promptForIngressController() - + assert result == "custom" - + def test_path_routing_no_controllers_defaults_to_default(self): """Test fallback to 'default' when no IngressControllers are found.""" app = create_mock_app() app.isInteractiveMode = True - + ingress_api = MagicMock() ingress_api.get.return_value = MagicMock(items=[]) app.dynamicClient.resources.get.return_value = ingress_api - + result = app._promptForIngressController() - + assert result == "default" - + def test_interactive_path_routing_complete_flow_with_configuration(self): """Test complete interactive flow for path routing with IngressController configuration.""" app = create_mock_app() @@ -157,7 +155,7 @@ def test_interactive_path_routing_complete_flow_with_configuration(self): app.showAdvancedOptions = True app.setParam("mas_channel", "9.2.0") app.setParam("mas_instance_id", "test-inst") - + # Mock IngressController API - not configured controller = create_ingress_controller_mock(namespace_ownership="Strict") ingress_api = MagicMock() @@ -167,13 +165,13 @@ def test_interactive_path_routing_complete_flow_with_configuration(self): controller # For configuration check ] app.dynamicClient.resources.get.return_value = ingress_api - + # Mock Ingress config for domain display ingress_config_api = MagicMock() ingress_config = MagicMock() ingress_config.spec.get.return_value = "apps.cluster.example.com" ingress_config_api.get.return_value = ingress_config - + with patch.object(app.dynamicClient.resources, 'get') as mock_get: def get_side_effect(api_version, kind): if kind == "IngressController": @@ -181,18 +179,18 @@ def get_side_effect(api_version, kind): elif kind == "Ingress": return ingress_config_api return MagicMock() - + mock_get.side_effect = get_side_effect - + # Mock isVersionEqualOrAfter to return True for version check with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): app.configRoutingMode() - + # Verify parameters are set correctly assert app.getParam("mas_routing_mode") == "path" assert app.getParam("mas_ingress_controller_name") == "default" assert app.getParam("mas_configure_ingress") == "true" - + def test_interactive_path_routing_user_declines_falls_back_to_subdomain(self): """Test fallback to subdomain when user declines to configure IngressController.""" app = create_mock_app() @@ -200,10 +198,10 @@ def test_interactive_path_routing_user_declines_falls_back_to_subdomain(self): app.showAdvancedOptions = True app.setParam("mas_channel", "9.2.0") app.setParam("mas_instance_id", "test-inst") - + # User declines to configure ingress app.yesOrNo.return_value = False - + # Mock IngressController not configured controller = create_ingress_controller_mock(namespace_ownership="Strict") ingress_api = MagicMock() @@ -213,13 +211,13 @@ def test_interactive_path_routing_user_declines_falls_back_to_subdomain(self): controller # For configuration check ] app.dynamicClient.resources.get.return_value = ingress_api - + # Mock Ingress config ingress_config_api = MagicMock() ingress_config = MagicMock() ingress_config.spec.get.return_value = "apps.cluster.example.com" ingress_config_api.get.return_value = ingress_config - + with patch.object(app.dynamicClient.resources, 'get') as mock_get: def get_side_effect(api_version, kind): if kind == "IngressController": @@ -227,17 +225,17 @@ def get_side_effect(api_version, kind): elif kind == "Ingress": return ingress_config_api return MagicMock() - + mock_get.side_effect = get_side_effect - + # Mock isVersionEqualOrAfter to return True for version check with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): app.configRoutingMode() - + # Verify fallback to subdomain assert app.getParam("mas_routing_mode") == "subdomain" assert app.getParam("mas_ingress_controller_name") == "" - + def test_interactive_path_routing_patch_fails_gracefully(self): """Test graceful failure when IngressController patch operation fails.""" app = create_mock_app() @@ -245,7 +243,7 @@ def test_interactive_path_routing_patch_fails_gracefully(self): app.showAdvancedOptions = True app.setParam("mas_channel", "9.2.0") app.setParam("mas_instance_id", "test-inst") - + # Mock IngressController not configured controller = create_ingress_controller_mock(namespace_ownership="Strict") ingress_api = MagicMock() @@ -254,13 +252,13 @@ def test_interactive_path_routing_patch_fails_gracefully(self): MagicMock(items=[controller]), # For listing controllers controller # For configuration check ] - + # Mock Ingress config ingress_config_api = MagicMock() ingress_config = MagicMock() ingress_config.spec.get.return_value = "apps.cluster.example.com" ingress_config_api.get.return_value = ingress_config - + with patch.object(app.dynamicClient.resources, 'get') as mock_get: def get_side_effect(api_version, kind): if kind == "IngressController": @@ -268,19 +266,19 @@ def get_side_effect(api_version, kind): elif kind == "Ingress": return ingress_config_api return MagicMock() - + mock_get.side_effect = get_side_effect - + with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): # User chooses to configure, but we'll simulate patch failure later app.yesOrNo.return_value = True app.configRoutingMode() - + # Even if patch fails later, parameters should be set for the pipeline to attempt assert app.getParam("mas_routing_mode") == "path" assert app.getParam("mas_configure_ingress") == "true" assert app.getParam("mas_ingress_controller_name") == "default" - + def test_interactive_path_routing_no_permissions_fails_gracefully(self): """Test graceful failure when user lacks permissions to check IngressController.""" app = create_mock_app() @@ -288,17 +286,17 @@ def test_interactive_path_routing_no_permissions_fails_gracefully(self): app.showAdvancedOptions = True app.setParam("mas_channel", "9.2.0") app.setParam("mas_instance_id", "test-inst") - + # Mock permission denied ingress_api = MagicMock() ingress_api.get.side_effect = ApiException(status=403, reason="Forbidden") - + # Mock Ingress config ingress_config_api = MagicMock() ingress_config = MagicMock() ingress_config.spec.get.return_value = "apps.cluster.example.com" ingress_config_api.get.return_value = ingress_config - + with patch.object(app.dynamicClient.resources, 'get') as mock_get: def get_side_effect(api_version, kind): if kind == "IngressController": @@ -306,12 +304,12 @@ def get_side_effect(api_version, kind): elif kind == "Ingress": return ingress_config_api return MagicMock() - + mock_get.side_effect = get_side_effect - + with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): app.configRoutingMode() - + # Should fall back to subdomain when permissions are denied assert app.getParam("mas_routing_mode") == "subdomain" assert app.getParam("mas_ingress_controller_name") == "" @@ -322,7 +320,7 @@ def get_side_effect(api_version, kind): # ============================================================================= class TestInteractiveSubdomainRouting: """Test suite for interactive mode subdomain routing scenarios.""" - + def test_interactive_subdomain_routing_selection(self): """Test direct selection of subdomain routing mode.""" app = create_mock_app() @@ -330,21 +328,21 @@ def test_interactive_subdomain_routing_selection(self): app.showAdvancedOptions = True app.setParam("mas_channel", "9.2.0") app.setParam("mas_instance_id", "test-inst") - + # User selects subdomain mode (option 2) app.promptForInt.return_value = 2 - + # Mock Ingress config ingress_config_api = MagicMock() ingress_config = MagicMock() ingress_config.spec.get.return_value = "apps.cluster.example.com" ingress_config_api.get.return_value = ingress_config app.dynamicClient.resources.get.return_value = ingress_config_api - + # Mock isVersionEqualOrAfter to return True for version check with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): app.configRoutingMode() - + # Verify subdomain mode is set assert app.getParam("mas_routing_mode") == "subdomain" assert app.getParam("mas_ingress_controller_name") == "" @@ -356,29 +354,29 @@ def test_interactive_subdomain_routing_selection(self): # ============================================================================= class TestNonInteractivePathRouting: """Test suite for non-interactive mode path-based routing scenarios.""" - + def test_noninteractive_path_mode_with_default_controller_configured(self): """Test non-interactive path mode with default controller already configured.""" app = create_mock_app() app.isInteractiveMode = False app.setParam("mas_routing_mode", "path") app.setParam("mas_ingress_controller_name", "default") - + # Mock controller already configured controller = create_ingress_controller_mock() ingress_api = MagicMock() ingress_api.get.return_value = controller app.dynamicClient.resources.get.return_value = ingress_api - + # Simulate validation logic canConfigure = app._checkIngressControllerPermissions("default") exists, isConfigured = app._checkIngressControllerForPathRouting("default") - + assert canConfigure is True assert exists is True assert isConfigured is True assert app.getParam("mas_configure_ingress") == "" - + def test_noninteractive_path_mode_with_configure_flag_success(self): """Test non-interactive path mode with --configure-ingress flag.""" app = create_mock_app() @@ -386,72 +384,72 @@ def test_noninteractive_path_mode_with_configure_flag_success(self): app.setParam("mas_routing_mode", "path") app.setParam("mas_ingress_controller_name", "default") app.setParam("mas_configure_ingress", "true") - + # Mock controller not configured controller = create_ingress_controller_mock(namespace_ownership="Strict") ingress_api = MagicMock() ingress_api.get.return_value = controller app.dynamicClient.resources.get.return_value = ingress_api - + canConfigure = app._checkIngressControllerPermissions("default") exists, isConfigured = app._checkIngressControllerForPathRouting("default") - + assert canConfigure is True assert exists is True assert isConfigured is False assert app.getParam("mas_configure_ingress") == "true" - + def test_noninteractive_path_mode_custom_controller_name(self): """Test non-interactive path mode with custom IngressController name.""" app = create_mock_app() app.isInteractiveMode = False app.setParam("mas_routing_mode", "path") app.setParam("mas_ingress_controller_name", "custom-ingress") - + # Mock custom controller configured controller = create_ingress_controller_mock("custom-ingress") ingress_api = MagicMock() ingress_api.get.return_value = controller app.dynamicClient.resources.get.return_value = ingress_api - + exists, isConfigured = app._checkIngressControllerForPathRouting("custom-ingress") - + assert exists is True assert isConfigured is True assert app.getParam("mas_ingress_controller_name") == "custom-ingress" - + def test_noninteractive_path_mode_missing_controller_name_defaults_to_default(self): """Test that missing controller name defaults to 'default'.""" app = create_mock_app() app.isInteractiveMode = False app.setParam("mas_routing_mode", "path") # Don't set mas_ingress_controller_name - + # Simulate the logic from app.py lines 1866-1876 ingressControllerName = app.getParam("mas_ingress_controller_name") if not ingressControllerName: ingressControllerName = "default" app.setParam("mas_ingress_controller_name", ingressControllerName) - + assert app.getParam("mas_ingress_controller_name") == "default" - + def test_noninteractive_path_mode_no_permissions_fails_gracefully(self): """Test non-interactive path mode fails gracefully when user lacks permissions.""" app = create_mock_app() app.isInteractiveMode = False app.setParam("mas_routing_mode", "path") app.setParam("mas_ingress_controller_name", "default") - + # Mock permission denied ingress_api = MagicMock() ingress_api.get.side_effect = ApiException(status=403, reason="Forbidden") app.dynamicClient.resources.get.return_value = ingress_api - + # Check permissions should return False result = app._checkIngressControllerPermissions() - + assert result is False - + def test_noninteractive_path_mode_with_configure_flag_no_permissions(self): """Test non-interactive path mode with --configure-ingress but no permissions.""" app = create_mock_app() @@ -459,18 +457,18 @@ def test_noninteractive_path_mode_with_configure_flag_no_permissions(self): app.setParam("mas_routing_mode", "path") app.setParam("mas_ingress_controller_name", "default") app.setParam("mas_configure_ingress", "true") - + # Mock permission denied ingress_api = MagicMock() ingress_api.get.side_effect = ApiException(status=403, reason="Forbidden") app.dynamicClient.resources.get.return_value = ingress_api - + # Check permissions should return False result = app._checkIngressControllerPermissions() - + # Should fail gracefully - permissions check returns False assert result is False - + def test_noninteractive_path_mode_controller_not_configured_without_flag(self): """Test non-interactive path mode when controller not configured and no --configure-ingress flag.""" app = create_mock_app() @@ -478,19 +476,19 @@ def test_noninteractive_path_mode_controller_not_configured_without_flag(self): app.setParam("mas_routing_mode", "path") app.setParam("mas_ingress_controller_name", "default") # Note: mas_configure_ingress is NOT set - + # Mock controller not configured controller = create_ingress_controller_mock(namespace_ownership="Strict") ingress_api = MagicMock() ingress_api.get.return_value = controller app.dynamicClient.resources.get.return_value = ingress_api - + # Check if configured _, is_configured = app._checkIngressControllerForPathRouting("default") - + # Should detect it's not configured assert is_configured is False - + def test_noninteractive_path_mode_all_flags_with_permissions(self): """Test non-interactive path mode with all flags and proper permissions.""" app = create_mock_app() @@ -498,47 +496,47 @@ def test_noninteractive_path_mode_all_flags_with_permissions(self): app.setParam("mas_routing_mode", "path") app.setParam("mas_ingress_controller_name", "custom-controller") app.setParam("mas_configure_ingress", "true") - + # Mock controller with permissions controller = create_ingress_controller_mock("custom-controller", namespace_ownership="Strict") ingress_api = MagicMock() ingress_api.get.return_value = controller app.dynamicClient.resources.get.return_value = ingress_api - + # Check permissions has_permissions = app._checkIngressControllerPermissions("custom-controller") - + # Check configuration status _, is_configured = app._checkIngressControllerForPathRouting("custom-controller") - + # Should have permissions but not be configured assert has_permissions is True assert is_configured is False - + # Parameters should remain as set assert app.getParam("mas_routing_mode") == "path" assert app.getParam("mas_ingress_controller_name") == "custom-controller" assert app.getParam("mas_configure_ingress") == "true" - + def test_noninteractive_path_mode_controller_already_configured(self): """Test non-interactive path mode when controller is already properly configured.""" app = create_mock_app() app.isInteractiveMode = False app.setParam("mas_routing_mode", "path") app.setParam("mas_ingress_controller_name", "default") - + # Mock controller already configured controller = create_ingress_controller_mock(namespace_ownership="InterNamespaceAllowed") ingress_api = MagicMock() ingress_api.get.return_value = controller app.dynamicClient.resources.get.return_value = ingress_api - + # Check if configured _, is_configured = app._checkIngressControllerForPathRouting("default") - + # Should be configured assert is_configured is True - + # mas_configure_ingress should not be needed assert app.getParam("mas_configure_ingress") == "" @@ -548,17 +546,17 @@ def test_noninteractive_path_mode_controller_already_configured(self): # ============================================================================= class TestNonInteractiveSubdomainRouting: """Test suite for non-interactive mode subdomain routing scenarios.""" - + def test_noninteractive_subdomain_mode_basic(self): """Test basic non-interactive subdomain mode.""" app = create_mock_app() app.isInteractiveMode = False app.setParam("mas_routing_mode", "subdomain") - + assert app.getParam("mas_routing_mode") == "subdomain" assert app.getParam("mas_ingress_controller_name") == "" assert app.getParam("mas_configure_ingress") == "" - + def test_noninteractive_subdomain_mode_ignores_ingress_flags(self): """Test that subdomain mode ignores ingress-related flags.""" app = create_mock_app() @@ -567,7 +565,7 @@ def test_noninteractive_subdomain_mode_ignores_ingress_flags(self): # These should be ignored/cleared for subdomain mode app.setParam("mas_ingress_controller_name", "") app.setParam("mas_configure_ingress", "") - + assert app.getParam("mas_routing_mode") == "subdomain" assert app.getParam("mas_ingress_controller_name") == "" assert app.getParam("mas_configure_ingress") == "" @@ -578,42 +576,42 @@ def test_noninteractive_subdomain_mode_ignores_ingress_flags(self): # ============================================================================= class TestIngressControllerPatchIntegration: """Test suite for configureIngressForPathBasedRouting integration.""" - + @patch('mas.devops.ocp.configureIngressForPathBasedRouting') def test_patch_function_called_with_correct_parameters(self, mock_configure): """Test that patch function is called with correct parameters.""" from mas.devops.ocp import configureIngressForPathBasedRouting - + mock_client = MagicMock() mock_configure.return_value = True - + result = configureIngressForPathBasedRouting(mock_client, "default") - + assert result is True mock_configure.assert_called_once_with(mock_client, "default") - + @patch('mas.devops.ocp.configureIngressForPathBasedRouting') def test_patch_function_handles_failure(self, mock_configure): """Test that patch function failure is handled correctly.""" from mas.devops.ocp import configureIngressForPathBasedRouting - + mock_client = MagicMock() mock_configure.return_value = False - + result = configureIngressForPathBasedRouting(mock_client, "default") - + assert result is False - + @patch('mas.devops.ocp.configureIngressForPathBasedRouting') def test_patch_function_with_custom_controller(self, mock_configure): """Test patch function with custom IngressController name.""" from mas.devops.ocp import configureIngressForPathBasedRouting - + mock_client = MagicMock() mock_configure.return_value = True - + result = configureIngressForPathBasedRouting(mock_client, "custom-ingress") - + assert result is True mock_configure.assert_called_once_with(mock_client, "custom-ingress") @@ -623,7 +621,7 @@ def test_patch_function_with_custom_controller(self, mock_configure): # ============================================================================= class TestCompleteCliFlow: """Integration tests for complete CLI prompt flow.""" - + def test_complete_flow_path_mode_with_all_prompts(self): """Test complete flow with all prompts for path mode.""" app = create_mock_app() @@ -631,23 +629,23 @@ def test_complete_flow_path_mode_with_all_prompts(self): app.showAdvancedOptions = True app.setParam("mas_channel", "9.2.0") app.setParam("mas_instance_id", "test-inst") - + # Mock multiple controllers controller1 = create_ingress_controller_mock("default", namespace_ownership="Strict") controller2 = create_ingress_controller_mock("custom", namespace_ownership="Strict") - + ingress_api = MagicMock() ingress_api.get.side_effect = [ controller1, # Permission check MagicMock(items=[controller1, controller2]), # List controllers controller1 # Configuration check ] - + ingress_config_api = MagicMock() ingress_config = MagicMock() ingress_config.spec.get.return_value = "apps.cluster.example.com" ingress_config_api.get.return_value = ingress_config - + with patch.object(app.dynamicClient.resources, 'get') as mock_get: def get_side_effect(api_version, kind): if kind == "IngressController": @@ -655,43 +653,41 @@ def get_side_effect(api_version, kind): elif kind == "Ingress": return ingress_config_api return MagicMock() - + mock_get.side_effect = get_side_effect - + # Mock isVersionEqualOrAfter to return True for version check with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): # Set up promptForInt to return different values for routing mode and controller selection app.promptForInt.side_effect = [1, 1] # Path mode, first controller app.configRoutingMode() - + # Verify all parameters are set correctly assert app.getParam("mas_routing_mode") == "path" assert app.getParam("mas_ingress_controller_name") == "default" assert app.getParam("mas_configure_ingress") == "true" - + # Verify prompts were called assert app.promptForInt.call_count == 2 assert app.yesOrNo.called - + def test_complete_flow_version_check_skips_routing_config(self): """Test that routing config is skipped for versions < 9.2.0.""" app = create_mock_app() app.isInteractiveMode = True app.showAdvancedOptions = True app.setParam("mas_channel", "9.1.0") # Version < 9.2.0 - + # configRoutingMode should not execute for this version # This is tested by checking the version condition from mas.devops.utils import isVersionEqualOrAfter - + should_configure = ( - app.showAdvancedOptions and - isVersionEqualOrAfter('9.2.0', app.getParam("mas_channel")) and - app.getParam("mas_channel") != '9.2.x-feature' + app.showAdvancedOptions and isVersionEqualOrAfter('9.2.0', app.getParam("mas_channel")) and app.getParam("mas_channel") != '9.2.x-feature' ) - + assert should_configure is False - + def test_complete_end_to_end_flow_with_advanced_options_and_patching(self): """Test complete end-to-end CLI flow: advanced options → routing prompt → path selection → patching.""" app = create_mock_app() @@ -699,7 +695,7 @@ def test_complete_end_to_end_flow_with_advanced_options_and_patching(self): app.showAdvancedOptions = True # This should trigger routing mode prompt app.setParam("mas_channel", "9.2.0") app.setParam("mas_instance_id", "test-inst") - + # Mock IngressController not configured (needs patching) controller = create_ingress_controller_mock(namespace_ownership="Strict") ingress_api = MagicMock() @@ -708,13 +704,13 @@ def test_complete_end_to_end_flow_with_advanced_options_and_patching(self): MagicMock(items=[controller]), # For listing controllers controller # For configuration check ] - + # Mock Ingress config ingress_config_api = MagicMock() ingress_config = MagicMock() ingress_config.spec.get.return_value = "apps.cluster.example.com" ingress_config_api.get.return_value = ingress_config - + with patch.object(app.dynamicClient.resources, 'get') as mock_get: def get_side_effect(api_version, kind): if kind == "IngressController": @@ -722,26 +718,26 @@ def get_side_effect(api_version, kind): elif kind == "Ingress": return ingress_config_api return MagicMock() - + mock_get.side_effect = get_side_effect - + # Mock the patching function from python-devops with patch('mas.devops.ocp.configureIngressForPathBasedRouting') as mock_patch: mock_patch.return_value = True # Patch succeeds - + with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): # User selects path mode (option 1) and agrees to configure app.promptForInt.return_value = 1 app.yesOrNo.return_value = True - + # Call configRoutingMode - this is what happens during CLI flow app.configRoutingMode() - + # Verify routing mode was configured assert app.getParam("mas_routing_mode") == "path" assert app.getParam("mas_ingress_controller_name") == "default" assert app.getParam("mas_configure_ingress") == "true" - + # Now simulate the actual patching that would happen in the pipeline # This is what the ansible playbook would call if app.getParam("mas_configure_ingress") == "true": @@ -750,14 +746,14 @@ def get_side_effect(api_version, kind): app.dynamicClient, app.getParam("mas_ingress_controller_name") ) - + # Verify the patch function was called with correct parameters mock_patch.assert_called_once_with( app.dynamicClient, "default" ) assert result is True - + def test_complete_end_to_end_flow_advanced_options_disabled_skips_routing(self): """Test that routing mode prompt is skipped when advanced options are disabled.""" app = create_mock_app() @@ -765,21 +761,21 @@ def test_complete_end_to_end_flow_advanced_options_disabled_skips_routing(self): app.showAdvancedOptions = False # Advanced options disabled app.setParam("mas_channel", "9.2.0") app.setParam("mas_instance_id", "test-inst") - + # Mock Ingress config (for _getMasDomainForDisplay if called) ingress_config_api = MagicMock() ingress_config = MagicMock() ingress_config.spec.get.return_value = "apps.cluster.example.com" ingress_config_api.get.return_value = ingress_config app.dynamicClient.resources.get.return_value = ingress_config_api - + with patch('mas.cli.install.app.isVersionEqualOrAfter', return_value=True): app.configRoutingMode() - + # Routing mode should not be set (method should exit early) # Default is subdomain if not explicitly set assert app.getParam("mas_routing_mode") == "" - + # promptForInt should not be called (no routing mode prompt) assert app.promptForInt.call_count == 0 @@ -789,88 +785,88 @@ def test_complete_end_to_end_flow_advanced_options_disabled_skips_routing(self): # ============================================================================= class TestRegressionPipelineFailures: """Regression tests to ensure changes don't break routing functionality.""" - + def test_regression_path_mode_parameters_preserved(self): """Test that path mode parameters are preserved throughout the flow.""" app = create_mock_app() - + # Set initial parameters app.setParam("mas_routing_mode", "path") app.setParam("mas_ingress_controller_name", "default") app.setParam("mas_configure_ingress", "true") - + # Simulate parameter access multiple times for _ in range(5): assert app.getParam("mas_routing_mode") == "path" assert app.getParam("mas_ingress_controller_name") == "default" assert app.getParam("mas_configure_ingress") == "true" - + def test_regression_subdomain_mode_parameters_preserved(self): """Test that subdomain mode parameters are preserved throughout the flow.""" app = create_mock_app() - + # Set initial parameters app.setParam("mas_routing_mode", "subdomain") app.setParam("mas_ingress_controller_name", "") - + # Simulate parameter access multiple times for _ in range(5): assert app.getParam("mas_routing_mode") == "subdomain" assert app.getParam("mas_ingress_controller_name") == "" - + def test_regression_invalid_routing_mode_not_set(self): """Test that invalid routing modes are not accepted.""" app = create_mock_app() - + # Try to set invalid mode app.setParam("mas_routing_mode", "invalid") - + # In actual implementation, this should be validated # For now, we just verify it's set (validation happens in argParser) assert app.getParam("mas_routing_mode") == "invalid" - + def test_regression_missing_required_parameters_for_path_mode(self): """Test detection of missing required parameters for path mode.""" app = create_mock_app() - + # Set path mode without controller name app.setParam("mas_routing_mode", "path") # Don't set mas_ingress_controller_name - + # Should default to 'default' if not set controller_name = app.getParam("mas_ingress_controller_name") if not controller_name: controller_name = "default" - + assert controller_name == "default" - + def test_regression_configure_flag_without_path_mode(self): """Test that configure flag without path mode is handled correctly.""" app = create_mock_app() - + # Set configure flag but use subdomain mode app.setParam("mas_routing_mode", "subdomain") app.setParam("mas_configure_ingress", "true") - + # Configure flag should be ignored for subdomain mode # In actual implementation, this should be validated assert app.getParam("mas_routing_mode") == "subdomain" assert app.getParam("mas_configure_ingress") == "true" # Set but ignored - + @patch('mas.devops.ocp.configureIngressForPathBasedRouting') def test_regression_patch_failure_does_not_break_pipeline(self, mock_configure): """Test that patch failure is handled gracefully and doesn't break pipeline.""" mock_configure.return_value = False - + app = create_mock_app() app.setParam("mas_routing_mode", "path") app.setParam("mas_ingress_controller_name", "default") app.setParam("mas_configure_ingress", "true") - + # Simulate patch failure from mas.devops.ocp import configureIngressForPathBasedRouting result = configureIngressForPathBasedRouting(app.dynamicClient, "default") - + # Should return False but not raise exception assert result is False @@ -880,97 +876,100 @@ def test_regression_patch_failure_does_not_break_pipeline(self, mock_configure): # ============================================================================= class TestEdgeCasesAndErrors: """Test edge cases and error scenarios.""" - + def test_edge_case_empty_routing_mode(self): """Test handling of empty routing mode.""" app = create_mock_app() - + # Don't set routing mode assert app.getParam("mas_routing_mode") == "" - + def test_edge_case_controller_name_with_special_characters(self): """Test IngressController name with special characters.""" app = create_mock_app() - + # Set controller name with hyphens (valid) app.setParam("mas_ingress_controller_name", "my-custom-ingress-123") assert app.getParam("mas_ingress_controller_name") == "my-custom-ingress-123" - + def test_edge_case_multiple_ingress_controllers_same_domain(self): """Test handling of multiple controllers with same domain.""" app = create_mock_app() - + controller1 = create_ingress_controller_mock("default", "apps.cluster.com") controller2 = create_ingress_controller_mock("custom", "apps.cluster.com") - + ingress_api = MagicMock() ingress_api.get.return_value = MagicMock(items=[controller1, controller2]) app.dynamicClient.resources.get.return_value = ingress_api - + # User selects option 2 (custom controller) app.promptForInt.return_value = 2 result = app._promptForIngressController() - + # Should still allow selection (user selected option 2 = custom) assert result == "custom" - + def test_edge_case_ingress_controller_api_exception(self): """Test handling of API exception when accessing IngressController.""" app = create_mock_app() - + ingress_api = MagicMock() ingress_api.get.side_effect = ApiException("API Error") app.dynamicClient.resources.get.return_value = ingress_api - + result = app._checkIngressControllerPermissions("default") - + # Should return False on exception assert result is False - + def test_edge_case_ingress_controller_not_found_exception(self): """Test handling of NotFoundError for IngressController.""" app = create_mock_app() - + ingress_api = MagicMock() ingress_api.get.side_effect = NotFoundError(MagicMock()) app.dynamicClient.resources.get.return_value = ingress_api - + exists, configured = app._checkIngressControllerForPathRouting("nonexistent") - + assert exists is False assert configured is False - + def test_edge_case_mas_domain_display_with_missing_ingress(self): """Test domain display when Ingress config is missing.""" app = create_mock_app() app.setParam("mas_instance_id", "test-inst") - + ingress_api = MagicMock() ingress_api.get.side_effect = Exception("Not found") app.dynamicClient.resources.get.return_value = ingress_api - + # Should fallback to default domain domain = app._getMasDomainForDisplay() - + assert "test-inst" in domain assert "yourdomain.com" in domain # ============================================================================= # Parameter Validation Tests # ============================================================================= + + class TestParameterValidation: """Test parameter validation for routing mode.""" - + def test_valid_routing_modes(self): """Test that only valid routing modes are accepted.""" app = create_mock_app() - + valid_modes = ["path", "subdomain"] - + for mode in valid_modes: app.setParam("mas_routing_mode", mode) assert app.getParam("mas_routing_mode") == mode + if __name__ == '__main__': pytest.main([__file__, '-v']) diff --git a/python/test/utils/install_test_helper.py b/python/test/utils/install_test_helper.py index 57aa93419be..26412ee4cb6 100644 --- a/python/test/utils/install_test_helper.py +++ b/python/test/utils/install_test_helper.py @@ -205,7 +205,7 @@ def setup_mocks(self): ingress_controller.spec.routeAdmission = MagicMock() # Set to 'Strict' initially (not configured for path-based routing) ingress_controller.spec.routeAdmission.namespaceOwnership = 'Strict' - + # Support dict-style access for _checkIngressControllerForPathRouting # Initially returns 'Strict' (not configured) def ingress_controller_get(key, default=None): @@ -219,9 +219,9 @@ def ingress_controller_get(key, default=None): 'get': lambda self, k, d=None: spec_dict.get(k, d) })() return default - + ingress_controller.get = ingress_controller_get - + # Configure get() to return single controller when queried by name # and list when queried without name def ingress_controller_api_get(**kwargs): @@ -233,9 +233,9 @@ def ingress_controller_api_get(**kwargs): ingress_controller_list = MagicMock() ingress_controller_list.items = [ingress_controller] return ingress_controller_list - + ingress_controller_api.get.side_effect = ingress_controller_api_get - + # Mock patch operation to succeed ingress_controller_api.patch = MagicMock(return_value=ingress_controller) @@ -309,7 +309,7 @@ def run_install_test(self): get_current_catalog.return_value = self.config.current_catalog launch_install_pipeline.return_value = 'https://pipeline.test.maximo.ibm.com' is_sno.return_value = self.config.is_sno - configure_ingress.return_value = True + configure_ingress.return_value = True is_version_equal_or_after.return_value = True # Configure PromptSession mock From 1895ecd98dc4cceb908da6facd1e494548e30c91 Mon Sep 17 00:00:00 2001 From: AquaD17 Date: Wed, 11 Feb 2026 14:45:13 +0530 Subject: [PATCH 07/13] use the arg in non interactive mode --- python/test/install/test_routing_mode.py | 228 ++++++++++++++++++++--- 1 file changed, 202 insertions(+), 26 deletions(-) diff --git a/python/test/install/test_routing_mode.py b/python/test/install/test_routing_mode.py index 30564ccb3e9..130e99ba6ee 100644 --- a/python/test/install/test_routing_mode.py +++ b/python/test/install/test_routing_mode.py @@ -15,6 +15,7 @@ """ from mas.cli.install.app import InstallApp +from mas.cli.install.argParser import installArgParser import sys import os import pytest @@ -378,12 +379,55 @@ def test_noninteractive_path_mode_with_default_controller_configured(self): assert app.getParam("mas_configure_ingress") == "" def test_noninteractive_path_mode_with_configure_flag_success(self): - """Test non-interactive path mode with --configure-ingress flag.""" + """Test non-interactive path mode with --configure-ingress flag. + + This test validates that when CLI arguments are provided: + - --routing path + - --ingress-controller-name default + - --configure-ingress + + The system properly sets the corresponding parameters: + - mas_routing_mode = "path" + - mas_ingress_controller_name = "default" + - mas_configure_ingress = "true" + """ + # Simulate CLI arguments for non-interactive path mode with configure flag + argv = [ + "--mas-instance-id", "testinst", + "--mas-workspace-id", "testws", + "--mas-channel", "9.2.0", + "--routing", "path", + "--ingress-controller-name", "default", + "--configure-ingress", + "--accept-license", + "--no-confirm" + ] + + # Parse arguments + args = installArgParser.parse_args(args=argv) + + # Verify args are parsed correctly + assert args.mas_routing_mode == "path", "Routing mode should be 'path'" + assert args.mas_ingress_controller_name == "default", "Controller name should be 'default'" + assert args.mas_configure_ingress is True, "--configure-ingress flag should be True" + + # Create app and simulate parameter setting from args app = create_mock_app() app.isInteractiveMode = False - app.setParam("mas_routing_mode", "path") - app.setParam("mas_ingress_controller_name", "default") - app.setParam("mas_configure_ingress", "true") + app.args = args + + # Simulate the parameter setting logic from app.py + app.setParam("mas_routing_mode", args.mas_routing_mode) + app.setParam("mas_ingress_controller_name", args.mas_ingress_controller_name) + + # Simulate lines 1809-1810 from app.py + if hasattr(args, 'mas_configure_ingress') and args.mas_configure_ingress: + app.setParam("mas_configure_ingress", "true") + + # Verify parameters are set correctly + assert app.getParam("mas_routing_mode") == "path", "mas_routing_mode parameter should be 'path'" + assert app.getParam("mas_ingress_controller_name") == "default", "mas_ingress_controller_name should be 'default'" + assert app.getParam("mas_configure_ingress") == "true", "mas_configure_ingress should be 'true'" # Mock controller not configured controller = create_ingress_controller_mock(namespace_ownership="Strict") @@ -391,20 +435,36 @@ def test_noninteractive_path_mode_with_configure_flag_success(self): ingress_api.get.return_value = controller app.dynamicClient.resources.get.return_value = ingress_api + # Verify IngressController checks work correctly canConfigure = app._checkIngressControllerPermissions("default") exists, isConfigured = app._checkIngressControllerForPathRouting("default") - assert canConfigure is True - assert exists is True - assert isConfigured is False - assert app.getParam("mas_configure_ingress") == "true" + assert canConfigure is True, "Should have permissions to configure" + assert exists is True, "Controller should exist" + assert isConfigured is False, "Controller should not be configured (Strict ownership)" def test_noninteractive_path_mode_custom_controller_name(self): """Test non-interactive path mode with custom IngressController name.""" + # Simulate CLI arguments with custom controller + argv = [ + "--mas-instance-id", "testinst", + "--mas-workspace-id", "testws", + "--mas-channel", "9.2.0", + "--routing", "path", + "--ingress-controller-name", "custom-ingress", + "--accept-license", + "--no-confirm" + ] + + args = installArgParser.parse_args(args=argv) + assert args.mas_routing_mode == "path" + assert args.mas_ingress_controller_name == "custom-ingress" + app = create_mock_app() app.isInteractiveMode = False - app.setParam("mas_routing_mode", "path") - app.setParam("mas_ingress_controller_name", "custom-ingress") + app.args = args + app.setParam("mas_routing_mode", args.mas_routing_mode) + app.setParam("mas_ingress_controller_name", args.mas_ingress_controller_name) # Mock custom controller configured controller = create_ingress_controller_mock("custom-ingress") @@ -420,10 +480,25 @@ def test_noninteractive_path_mode_custom_controller_name(self): def test_noninteractive_path_mode_missing_controller_name_defaults_to_default(self): """Test that missing controller name defaults to 'default'.""" + # Simulate CLI arguments without --ingress-controller-name + argv = [ + "--mas-instance-id", "testinst", + "--mas-workspace-id", "testws", + "--mas-channel", "9.2.0", + "--routing", "path", + "--accept-license", + "--no-confirm" + ] + + args = installArgParser.parse_args(args=argv) + assert args.mas_routing_mode == "path" + # ingress_controller_name should be None when not provided + assert args.mas_ingress_controller_name is None + app = create_mock_app() app.isInteractiveMode = False - app.setParam("mas_routing_mode", "path") - # Don't set mas_ingress_controller_name + app.args = args + app.setParam("mas_routing_mode", args.mas_routing_mode) # Simulate the logic from app.py lines 1866-1876 ingressControllerName = app.getParam("mas_ingress_controller_name") @@ -435,10 +510,23 @@ def test_noninteractive_path_mode_missing_controller_name_defaults_to_default(se def test_noninteractive_path_mode_no_permissions_fails_gracefully(self): """Test non-interactive path mode fails gracefully when user lacks permissions.""" + argv = [ + "--mas-instance-id", "testinst", + "--mas-workspace-id", "testws", + "--mas-channel", "9.2.0", + "--routing", "path", + "--ingress-controller-name", "default", + "--accept-license", + "--no-confirm" + ] + + args = installArgParser.parse_args(args=argv) + app = create_mock_app() app.isInteractiveMode = False - app.setParam("mas_routing_mode", "path") - app.setParam("mas_ingress_controller_name", "default") + app.args = args + app.setParam("mas_routing_mode", args.mas_routing_mode) + app.setParam("mas_ingress_controller_name", args.mas_ingress_controller_name) # Mock permission denied ingress_api = MagicMock() @@ -452,11 +540,27 @@ def test_noninteractive_path_mode_no_permissions_fails_gracefully(self): def test_noninteractive_path_mode_with_configure_flag_no_permissions(self): """Test non-interactive path mode with --configure-ingress but no permissions.""" + argv = [ + "--mas-instance-id", "testinst", + "--mas-workspace-id", "testws", + "--mas-channel", "9.2.0", + "--routing", "path", + "--ingress-controller-name", "default", + "--configure-ingress", + "--accept-license", + "--no-confirm" + ] + + args = installArgParser.parse_args(args=argv) + app = create_mock_app() app.isInteractiveMode = False - app.setParam("mas_routing_mode", "path") - app.setParam("mas_ingress_controller_name", "default") - app.setParam("mas_configure_ingress", "true") + app.args = args + app.setParam("mas_routing_mode", args.mas_routing_mode) + app.setParam("mas_ingress_controller_name", args.mas_ingress_controller_name) + + if hasattr(args, 'mas_configure_ingress') and args.mas_configure_ingress: + app.setParam("mas_configure_ingress", "true") # Mock permission denied ingress_api = MagicMock() @@ -471,10 +575,24 @@ def test_noninteractive_path_mode_with_configure_flag_no_permissions(self): def test_noninteractive_path_mode_controller_not_configured_without_flag(self): """Test non-interactive path mode when controller not configured and no --configure-ingress flag.""" + argv = [ + "--mas-instance-id", "testinst", + "--mas-workspace-id", "testws", + "--mas-channel", "9.2.0", + "--routing", "path", + "--ingress-controller-name", "default", + # Note: --configure-ingress is NOT provided + "--accept-license", + "--no-confirm" + ] + + args = installArgParser.parse_args(args=argv) + app = create_mock_app() app.isInteractiveMode = False - app.setParam("mas_routing_mode", "path") - app.setParam("mas_ingress_controller_name", "default") + app.args = args + app.setParam("mas_routing_mode", args.mas_routing_mode) + app.setParam("mas_ingress_controller_name", args.mas_ingress_controller_name) # Note: mas_configure_ingress is NOT set # Mock controller not configured @@ -491,11 +609,27 @@ def test_noninteractive_path_mode_controller_not_configured_without_flag(self): def test_noninteractive_path_mode_all_flags_with_permissions(self): """Test non-interactive path mode with all flags and proper permissions.""" + argv = [ + "--mas-instance-id", "testinst", + "--mas-workspace-id", "testws", + "--mas-channel", "9.2.0", + "--routing", "path", + "--ingress-controller-name", "custom-controller", + "--configure-ingress", + "--accept-license", + "--no-confirm" + ] + + args = installArgParser.parse_args(args=argv) + app = create_mock_app() app.isInteractiveMode = False - app.setParam("mas_routing_mode", "path") - app.setParam("mas_ingress_controller_name", "custom-controller") - app.setParam("mas_configure_ingress", "true") + app.args = args + app.setParam("mas_routing_mode", args.mas_routing_mode) + app.setParam("mas_ingress_controller_name", args.mas_ingress_controller_name) + + if hasattr(args, 'mas_configure_ingress') and args.mas_configure_ingress: + app.setParam("mas_configure_ingress", "true") # Mock controller with permissions controller = create_ingress_controller_mock("custom-controller", namespace_ownership="Strict") @@ -520,10 +654,23 @@ def test_noninteractive_path_mode_all_flags_with_permissions(self): def test_noninteractive_path_mode_controller_already_configured(self): """Test non-interactive path mode when controller is already properly configured.""" + argv = [ + "--mas-instance-id", "testinst", + "--mas-workspace-id", "testws", + "--mas-channel", "9.2.0", + "--routing", "path", + "--ingress-controller-name", "default", + "--accept-license", + "--no-confirm" + ] + + args = installArgParser.parse_args(args=argv) + app = create_mock_app() app.isInteractiveMode = False - app.setParam("mas_routing_mode", "path") - app.setParam("mas_ingress_controller_name", "default") + app.args = args + app.setParam("mas_routing_mode", args.mas_routing_mode) + app.setParam("mas_ingress_controller_name", args.mas_ingress_controller_name) # Mock controller already configured controller = create_ingress_controller_mock(namespace_ownership="InterNamespaceAllowed") @@ -549,9 +696,22 @@ class TestNonInteractiveSubdomainRouting: def test_noninteractive_subdomain_mode_basic(self): """Test basic non-interactive subdomain mode.""" + argv = [ + "--mas-instance-id", "testinst", + "--mas-workspace-id", "testws", + "--mas-channel", "9.2.0", + "--routing", "subdomain", + "--accept-license", + "--no-confirm" + ] + + args = installArgParser.parse_args(args=argv) + assert args.mas_routing_mode == "subdomain" + app = create_mock_app() app.isInteractiveMode = False - app.setParam("mas_routing_mode", "subdomain") + app.args = args + app.setParam("mas_routing_mode", args.mas_routing_mode) assert app.getParam("mas_routing_mode") == "subdomain" assert app.getParam("mas_ingress_controller_name") == "" @@ -559,9 +719,25 @@ def test_noninteractive_subdomain_mode_basic(self): def test_noninteractive_subdomain_mode_ignores_ingress_flags(self): """Test that subdomain mode ignores ingress-related flags.""" + argv = [ + "--mas-instance-id", "testinst", + "--mas-workspace-id", "testws", + "--mas-channel", "9.2.0", + "--routing", "subdomain", + # Even if these are provided, they should be ignored for subdomain mode + "--ingress-controller-name", "default", + "--configure-ingress", + "--accept-license", + "--no-confirm" + ] + + args = installArgParser.parse_args(args=argv) + assert args.mas_routing_mode == "subdomain" + app = create_mock_app() app.isInteractiveMode = False - app.setParam("mas_routing_mode", "subdomain") + app.args = args + app.setParam("mas_routing_mode", args.mas_routing_mode) # These should be ignored/cleared for subdomain mode app.setParam("mas_ingress_controller_name", "") app.setParam("mas_configure_ingress", "") From fe5e1b59b0bc59bd08545b06c53d7e9c000f4f45 Mon Sep 17 00:00:00 2001 From: AquaD17 Date: Wed, 11 Feb 2026 14:46:18 +0530 Subject: [PATCH 08/13] fix the pre-commit --- python/test/install/test_routing_mode.py | 54 ++++++++++++------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/python/test/install/test_routing_mode.py b/python/test/install/test_routing_mode.py index 130e99ba6ee..20a0abf8aac 100644 --- a/python/test/install/test_routing_mode.py +++ b/python/test/install/test_routing_mode.py @@ -380,12 +380,12 @@ def test_noninteractive_path_mode_with_default_controller_configured(self): def test_noninteractive_path_mode_with_configure_flag_success(self): """Test non-interactive path mode with --configure-ingress flag. - + This test validates that when CLI arguments are provided: - --routing path - --ingress-controller-name default - --configure-ingress - + The system properly sets the corresponding parameters: - mas_routing_mode = "path" - mas_ingress_controller_name = "default" @@ -402,24 +402,24 @@ def test_noninteractive_path_mode_with_configure_flag_success(self): "--accept-license", "--no-confirm" ] - + # Parse arguments args = installArgParser.parse_args(args=argv) - + # Verify args are parsed correctly assert args.mas_routing_mode == "path", "Routing mode should be 'path'" assert args.mas_ingress_controller_name == "default", "Controller name should be 'default'" assert args.mas_configure_ingress is True, "--configure-ingress flag should be True" - + # Create app and simulate parameter setting from args app = create_mock_app() app.isInteractiveMode = False app.args = args - + # Simulate the parameter setting logic from app.py app.setParam("mas_routing_mode", args.mas_routing_mode) app.setParam("mas_ingress_controller_name", args.mas_ingress_controller_name) - + # Simulate lines 1809-1810 from app.py if hasattr(args, 'mas_configure_ingress') and args.mas_configure_ingress: app.setParam("mas_configure_ingress", "true") @@ -455,11 +455,11 @@ def test_noninteractive_path_mode_custom_controller_name(self): "--accept-license", "--no-confirm" ] - + args = installArgParser.parse_args(args=argv) assert args.mas_routing_mode == "path" assert args.mas_ingress_controller_name == "custom-ingress" - + app = create_mock_app() app.isInteractiveMode = False app.args = args @@ -489,12 +489,12 @@ def test_noninteractive_path_mode_missing_controller_name_defaults_to_default(se "--accept-license", "--no-confirm" ] - + args = installArgParser.parse_args(args=argv) assert args.mas_routing_mode == "path" # ingress_controller_name should be None when not provided assert args.mas_ingress_controller_name is None - + app = create_mock_app() app.isInteractiveMode = False app.args = args @@ -519,9 +519,9 @@ def test_noninteractive_path_mode_no_permissions_fails_gracefully(self): "--accept-license", "--no-confirm" ] - + args = installArgParser.parse_args(args=argv) - + app = create_mock_app() app.isInteractiveMode = False app.args = args @@ -550,15 +550,15 @@ def test_noninteractive_path_mode_with_configure_flag_no_permissions(self): "--accept-license", "--no-confirm" ] - + args = installArgParser.parse_args(args=argv) - + app = create_mock_app() app.isInteractiveMode = False app.args = args app.setParam("mas_routing_mode", args.mas_routing_mode) app.setParam("mas_ingress_controller_name", args.mas_ingress_controller_name) - + if hasattr(args, 'mas_configure_ingress') and args.mas_configure_ingress: app.setParam("mas_configure_ingress", "true") @@ -585,9 +585,9 @@ def test_noninteractive_path_mode_controller_not_configured_without_flag(self): "--accept-license", "--no-confirm" ] - + args = installArgParser.parse_args(args=argv) - + app = create_mock_app() app.isInteractiveMode = False app.args = args @@ -619,15 +619,15 @@ def test_noninteractive_path_mode_all_flags_with_permissions(self): "--accept-license", "--no-confirm" ] - + args = installArgParser.parse_args(args=argv) - + app = create_mock_app() app.isInteractiveMode = False app.args = args app.setParam("mas_routing_mode", args.mas_routing_mode) app.setParam("mas_ingress_controller_name", args.mas_ingress_controller_name) - + if hasattr(args, 'mas_configure_ingress') and args.mas_configure_ingress: app.setParam("mas_configure_ingress", "true") @@ -663,9 +663,9 @@ def test_noninteractive_path_mode_controller_already_configured(self): "--accept-license", "--no-confirm" ] - + args = installArgParser.parse_args(args=argv) - + app = create_mock_app() app.isInteractiveMode = False app.args = args @@ -704,10 +704,10 @@ def test_noninteractive_subdomain_mode_basic(self): "--accept-license", "--no-confirm" ] - + args = installArgParser.parse_args(args=argv) assert args.mas_routing_mode == "subdomain" - + app = create_mock_app() app.isInteractiveMode = False app.args = args @@ -730,10 +730,10 @@ def test_noninteractive_subdomain_mode_ignores_ingress_flags(self): "--accept-license", "--no-confirm" ] - + args = installArgParser.parse_args(args=argv) assert args.mas_routing_mode == "subdomain" - + app = create_mock_app() app.isInteractiveMode = False app.args = args From d854d0706e3226d117c33392e5b5a51b4699d175 Mon Sep 17 00:00:00 2001 From: AquaD17 Date: Wed, 11 Feb 2026 15:27:30 +0530 Subject: [PATCH 09/13] remove some unnecessary test cases --- python/test/install/test_routing_mode.py | 173 ----------------------- 1 file changed, 173 deletions(-) diff --git a/python/test/install/test_routing_mode.py b/python/test/install/test_routing_mode.py index 20a0abf8aac..338d3cdab09 100644 --- a/python/test/install/test_routing_mode.py +++ b/python/test/install/test_routing_mode.py @@ -955,183 +955,10 @@ def test_complete_end_to_end_flow_advanced_options_disabled_skips_routing(self): # promptForInt should not be called (no routing mode prompt) assert app.promptForInt.call_count == 0 - -# ============================================================================= -# Regression Tests - Pipeline Failure Scenarios -# ============================================================================= -class TestRegressionPipelineFailures: - """Regression tests to ensure changes don't break routing functionality.""" - - def test_regression_path_mode_parameters_preserved(self): - """Test that path mode parameters are preserved throughout the flow.""" - app = create_mock_app() - - # Set initial parameters - app.setParam("mas_routing_mode", "path") - app.setParam("mas_ingress_controller_name", "default") - app.setParam("mas_configure_ingress", "true") - - # Simulate parameter access multiple times - for _ in range(5): - assert app.getParam("mas_routing_mode") == "path" - assert app.getParam("mas_ingress_controller_name") == "default" - assert app.getParam("mas_configure_ingress") == "true" - - def test_regression_subdomain_mode_parameters_preserved(self): - """Test that subdomain mode parameters are preserved throughout the flow.""" - app = create_mock_app() - - # Set initial parameters - app.setParam("mas_routing_mode", "subdomain") - app.setParam("mas_ingress_controller_name", "") - - # Simulate parameter access multiple times - for _ in range(5): - assert app.getParam("mas_routing_mode") == "subdomain" - assert app.getParam("mas_ingress_controller_name") == "" - - def test_regression_invalid_routing_mode_not_set(self): - """Test that invalid routing modes are not accepted.""" - app = create_mock_app() - - # Try to set invalid mode - app.setParam("mas_routing_mode", "invalid") - - # In actual implementation, this should be validated - # For now, we just verify it's set (validation happens in argParser) - assert app.getParam("mas_routing_mode") == "invalid" - - def test_regression_missing_required_parameters_for_path_mode(self): - """Test detection of missing required parameters for path mode.""" - app = create_mock_app() - - # Set path mode without controller name - app.setParam("mas_routing_mode", "path") - # Don't set mas_ingress_controller_name - - # Should default to 'default' if not set - controller_name = app.getParam("mas_ingress_controller_name") - if not controller_name: - controller_name = "default" - - assert controller_name == "default" - - def test_regression_configure_flag_without_path_mode(self): - """Test that configure flag without path mode is handled correctly.""" - app = create_mock_app() - - # Set configure flag but use subdomain mode - app.setParam("mas_routing_mode", "subdomain") - app.setParam("mas_configure_ingress", "true") - - # Configure flag should be ignored for subdomain mode - # In actual implementation, this should be validated - assert app.getParam("mas_routing_mode") == "subdomain" - assert app.getParam("mas_configure_ingress") == "true" # Set but ignored - - @patch('mas.devops.ocp.configureIngressForPathBasedRouting') - def test_regression_patch_failure_does_not_break_pipeline(self, mock_configure): - """Test that patch failure is handled gracefully and doesn't break pipeline.""" - mock_configure.return_value = False - - app = create_mock_app() - app.setParam("mas_routing_mode", "path") - app.setParam("mas_ingress_controller_name", "default") - app.setParam("mas_configure_ingress", "true") - - # Simulate patch failure - from mas.devops.ocp import configureIngressForPathBasedRouting - result = configureIngressForPathBasedRouting(app.dynamicClient, "default") - - # Should return False but not raise exception - assert result is False - - -# ============================================================================= -# Edge Cases and Error Scenarios -# ============================================================================= -class TestEdgeCasesAndErrors: - """Test edge cases and error scenarios.""" - - def test_edge_case_empty_routing_mode(self): - """Test handling of empty routing mode.""" - app = create_mock_app() - - # Don't set routing mode - assert app.getParam("mas_routing_mode") == "" - - def test_edge_case_controller_name_with_special_characters(self): - """Test IngressController name with special characters.""" - app = create_mock_app() - - # Set controller name with hyphens (valid) - app.setParam("mas_ingress_controller_name", "my-custom-ingress-123") - assert app.getParam("mas_ingress_controller_name") == "my-custom-ingress-123" - - def test_edge_case_multiple_ingress_controllers_same_domain(self): - """Test handling of multiple controllers with same domain.""" - app = create_mock_app() - - controller1 = create_ingress_controller_mock("default", "apps.cluster.com") - controller2 = create_ingress_controller_mock("custom", "apps.cluster.com") - - ingress_api = MagicMock() - ingress_api.get.return_value = MagicMock(items=[controller1, controller2]) - app.dynamicClient.resources.get.return_value = ingress_api - - # User selects option 2 (custom controller) - app.promptForInt.return_value = 2 - result = app._promptForIngressController() - - # Should still allow selection (user selected option 2 = custom) - assert result == "custom" - - def test_edge_case_ingress_controller_api_exception(self): - """Test handling of API exception when accessing IngressController.""" - app = create_mock_app() - - ingress_api = MagicMock() - ingress_api.get.side_effect = ApiException("API Error") - app.dynamicClient.resources.get.return_value = ingress_api - - result = app._checkIngressControllerPermissions("default") - - # Should return False on exception - assert result is False - - def test_edge_case_ingress_controller_not_found_exception(self): - """Test handling of NotFoundError for IngressController.""" - app = create_mock_app() - - ingress_api = MagicMock() - ingress_api.get.side_effect = NotFoundError(MagicMock()) - app.dynamicClient.resources.get.return_value = ingress_api - - exists, configured = app._checkIngressControllerForPathRouting("nonexistent") - - assert exists is False - assert configured is False - - def test_edge_case_mas_domain_display_with_missing_ingress(self): - """Test domain display when Ingress config is missing.""" - app = create_mock_app() - app.setParam("mas_instance_id", "test-inst") - - ingress_api = MagicMock() - ingress_api.get.side_effect = Exception("Not found") - app.dynamicClient.resources.get.return_value = ingress_api - - # Should fallback to default domain - domain = app._getMasDomainForDisplay() - - assert "test-inst" in domain - assert "yourdomain.com" in domain - # ============================================================================= # Parameter Validation Tests # ============================================================================= - class TestParameterValidation: """Test parameter validation for routing mode.""" From a652e85df7b902ecf3b0f923a4e304b0bac2c334 Mon Sep 17 00:00:00 2001 From: AquaD17 Date: Wed, 11 Feb 2026 15:35:21 +0530 Subject: [PATCH 10/13] fix pre-commit --- python/test/install/test_routing_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/test/install/test_routing_mode.py b/python/test/install/test_routing_mode.py index 338d3cdab09..05997e503cd 100644 --- a/python/test/install/test_routing_mode.py +++ b/python/test/install/test_routing_mode.py @@ -20,7 +20,6 @@ import os import pytest from unittest.mock import MagicMock, patch -from openshift.dynamic.exceptions import NotFoundError from kubernetes.client.rest import ApiException sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) @@ -959,6 +958,7 @@ def test_complete_end_to_end_flow_advanced_options_disabled_skips_routing(self): # Parameter Validation Tests # ============================================================================= + class TestParameterValidation: """Test parameter validation for routing mode.""" From 806a2bc0700064c887507b48f8cba8f55093f472 Mon Sep 17 00:00:00 2001 From: David Parker Date: Mon, 16 Feb 2026 11:56:53 +0000 Subject: [PATCH 11/13] No need to switch to ExitStack modern Python versions have multi-with, which is cleaner --- python/test/utils/install_test_helper.py | 67 ++++++++++++------------ 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/python/test/utils/install_test_helper.py b/python/test/utils/install_test_helper.py index 26412ee4cb6..d8cea2446a4 100644 --- a/python/test/utils/install_test_helper.py +++ b/python/test/utils/install_test_helper.py @@ -11,7 +11,6 @@ import time import threading -from contextlib import ExitStack from typing import Dict, Callable, Optional from unittest import mock from unittest.mock import MagicMock @@ -281,26 +280,25 @@ def run_install_test(self): with mock.patch('mas.cli.cli.config'): dynamic_client, resource_apis = self.setup_mocks() - # Use ExitStack to manage multiple patches (avoids "too many nested blocks" error) - with ExitStack() as stack: - # Setup all patches - dynamic_client_class = stack.enter_context(mock.patch('mas.cli.cli.DynamicClient')) - get_nodes = stack.enter_context(mock.patch('mas.cli.cli.getNodes')) - is_airgap_install = stack.enter_context(mock.patch('mas.cli.cli.isAirgapInstall')) - get_current_catalog = stack.enter_context(mock.patch('mas.cli.install.app.getCurrentCatalog')) - is_version_equal_or_after = stack.enter_context(mock.patch('mas.cli.install.app.isVersionEqualOrAfter')) - stack.enter_context(mock.patch('mas.cli.install.app.installOpenShiftPipelines')) - stack.enter_context(mock.patch('mas.cli.install.app.updateTektonDefinitions')) - stack.enter_context(mock.patch('mas.cli.install.app.createNamespace')) - stack.enter_context(mock.patch('mas.cli.install.app.preparePipelinesNamespace')) - launch_install_pipeline = stack.enter_context(mock.patch('mas.cli.install.app.launchInstallPipeline')) - configure_ingress = stack.enter_context(mock.patch('mas.cli.install.app.configureIngressForPathBasedRouting')) - is_sno = stack.enter_context(mock.patch('mas.cli.cli.isSNO')) - mixins_prompt = stack.enter_context(mock.patch('mas.cli.displayMixins.prompt')) - prompt_session_class = stack.enter_context(mock.patch('mas.cli.displayMixins.PromptSession')) - app_prompt = stack.enter_context(mock.patch('mas.cli.install.app.prompt')) - get_storage_classes = stack.enter_context(mock.patch('mas.cli.install.app.getStorageClasses')) - get_default_storage_classes = stack.enter_context(mock.patch('mas.cli.install.app.getDefaultStorageClasses')) + with ( + mock.patch('mas.cli.cli.DynamicClient') as dynamic_client_class, + mock.patch('mas.cli.cli.getNodes') as get_nodes, + mock.patch('mas.cli.cli.isAirgapInstall') as is_airgap_install, + mock.patch('mas.cli.install.app.getCurrentCatalog') as get_current_catalog, + mock.patch('mas.cli.install.app.isVersionEqualOrAfter') as is_version_equal_or_after, + mock.patch('mas.cli.install.app.installOpenShiftPipelines'), + mock.patch('mas.cli.install.app.updateTektonDefinitions'), + mock.patch('mas.cli.install.app.createNamespace'), + mock.patch('mas.cli.install.app.preparePipelinesNamespace'), + mock.patch('mas.cli.install.app.launchInstallPipeline') as launch_install_pipeline, + mock.patch('mas.cli.install.app.configureIngressForPathBasedRouting') as configure_ingress, + mock.patch('mas.cli.cli.isSNO') as is_sno, + mock.patch('mas.cli.displayMixins.prompt') as mixins_prompt, + mock.patch('mas.cli.displayMixins.PromptSession') as prompt_session_class, + mock.patch('mas.cli.install.app.prompt') as app_prompt, + mock.patch('mas.cli.install.app.getStorageClasses') as get_storage_classes, + mock.patch('mas.cli.install.app.getDefaultStorageClasses') as get_default_storage_classes, + ): # Configure mock return values dynamic_client_class.return_value = dynamic_client @@ -343,22 +341,23 @@ def run_install_test(self): finally: self.stop_watchdog() - # Check if test timed out - if self.test_failed['message']: - raise TimeoutError(self.test_failed['message']) + # Check if test timed out + if self.test_failed['message']: + raise TimeoutError(self.test_failed['message']) - # Verify SystemExit was raised if expected - if self.config.expect_system_exit and not system_exit_raised: - raise AssertionError("Expected SystemExit to be raised but it was not") + # Verify SystemExit was raised if expected + if self.config.expect_system_exit and not system_exit_raised: + raise AssertionError("Expected SystemExit to be raised but it was not") - # Verify exit code is non-zero if SystemExit was expected - if self.config.expect_system_exit and exit_code == 0: - raise AssertionError(f"Expected non-zero exit code but got {exit_code}") + # Verify exit code is non-zero if SystemExit was expected + if self.config.expect_system_exit and exit_code == 0: + raise AssertionError(f"Expected non-zero exit code but got {exit_code}") - # Always verify all prompts were matched exactly once - # This will fail if any prompts weren't reached (e.g., due to early SystemExit) - # which is the desired behavior to ensure tests accurately reflect what prompts are shown - self.prompt_tracker.verify_all_prompts_matched() + # Always verify all prompts were matched exactly once + # This will fail if any prompts weren't reached (e.g., due to early SystemExit) + # which is the desired behavior to ensure tests accurately reflect what prompts are shown + assert self.prompt_tracker is not None, "prompt_tracker should be initialized" + self.prompt_tracker.verify_all_prompts_matched() def run_install_test(tmpdir, config: InstallTestConfig): From c28594b54be149de2e0f3a5249b896d6c7a43f2f Mon Sep 17 00:00:00 2001 From: David Parker Date: Mon, 16 Feb 2026 12:09:14 +0000 Subject: [PATCH 12/13] Fix indent --- python/test/utils/install_test_helper.py | 34 ++++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/python/test/utils/install_test_helper.py b/python/test/utils/install_test_helper.py index d8cea2446a4..02951ff9319 100644 --- a/python/test/utils/install_test_helper.py +++ b/python/test/utils/install_test_helper.py @@ -341,23 +341,23 @@ def run_install_test(self): finally: self.stop_watchdog() - # Check if test timed out - if self.test_failed['message']: - raise TimeoutError(self.test_failed['message']) - - # Verify SystemExit was raised if expected - if self.config.expect_system_exit and not system_exit_raised: - raise AssertionError("Expected SystemExit to be raised but it was not") - - # Verify exit code is non-zero if SystemExit was expected - if self.config.expect_system_exit and exit_code == 0: - raise AssertionError(f"Expected non-zero exit code but got {exit_code}") - - # Always verify all prompts were matched exactly once - # This will fail if any prompts weren't reached (e.g., due to early SystemExit) - # which is the desired behavior to ensure tests accurately reflect what prompts are shown - assert self.prompt_tracker is not None, "prompt_tracker should be initialized" - self.prompt_tracker.verify_all_prompts_matched() + # Check if test timed out + if self.test_failed['message']: + raise TimeoutError(self.test_failed['message']) + + # Verify SystemExit was raised if expected + if self.config.expect_system_exit and not system_exit_raised: + raise AssertionError("Expected SystemExit to be raised but it was not") + + # Verify exit code is non-zero if SystemExit was expected + if self.config.expect_system_exit and exit_code == 0: + raise AssertionError(f"Expected non-zero exit code but got {exit_code}") + + # Always verify all prompts were matched exactly once + # This will fail if any prompts weren't reached (e.g., due to early SystemExit) + # which is the desired behavior to ensure tests accurately reflect what prompts are shown + assert self.prompt_tracker is not None, "prompt_tracker should be initialized" + self.prompt_tracker.verify_all_prompts_matched() def run_install_test(tmpdir, config: InstallTestConfig): From c4728f028137aaf019120a436100060c216f58f6 Mon Sep 17 00:00:00 2001 From: David Parker Date: Mon, 16 Feb 2026 12:14:22 +0000 Subject: [PATCH 13/13] Remove isVersionEqualOrAfter mock --- python/test/utils/install_test_helper.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/test/utils/install_test_helper.py b/python/test/utils/install_test_helper.py index 02951ff9319..c79974cd105 100644 --- a/python/test/utils/install_test_helper.py +++ b/python/test/utils/install_test_helper.py @@ -285,7 +285,6 @@ def run_install_test(self): mock.patch('mas.cli.cli.getNodes') as get_nodes, mock.patch('mas.cli.cli.isAirgapInstall') as is_airgap_install, mock.patch('mas.cli.install.app.getCurrentCatalog') as get_current_catalog, - mock.patch('mas.cli.install.app.isVersionEqualOrAfter') as is_version_equal_or_after, mock.patch('mas.cli.install.app.installOpenShiftPipelines'), mock.patch('mas.cli.install.app.updateTektonDefinitions'), mock.patch('mas.cli.install.app.createNamespace'), @@ -308,7 +307,6 @@ def run_install_test(self): launch_install_pipeline.return_value = 'https://pipeline.test.maximo.ibm.com' is_sno.return_value = self.config.is_sno configure_ingress.return_value = True - is_version_equal_or_after.return_value = True # Configure PromptSession mock prompt_session_instance = MagicMock()