diff --git a/cypress/README.md b/cypress/README.md index ab970491638..d2e60db6ad9 100644 --- a/cypress/README.md +++ b/cypress/README.md @@ -94,6 +94,28 @@ ManageIQ implements the following cypress extensions: * `cy.validateFormFields(fieldConfigs)` - validates form input fields based on provided configurations. `fieldConfigs` is an array of field configuration objects with properties: `id` (required) - the ID of the form field, `fieldType` (optional, default: 'input') - the type of field ('input', 'select', 'textarea'), `inputFieldType` (optional, default: 'text') - the type of input field ('text', 'password', 'number'), `shouldBeDisabled` (optional, default: false) - whether the field should be disabled, `expectedValue` (optional) - the expected value of the field. e.g. `cy.validateFormFields([{ id: 'name', shouldBeDisabled: true }, { id: 'role', fieldType: 'select', expectedValue: 'admin' }]);` or using constants: `cy.validateFormFields([{ [FIELD_CONFIG_KEYS.ID]: 'email', [FIELD_CONFIG_KEYS.INPUT_FIELD_TYPE]: 'email' }, { [FIELD_CONFIG_KEYS.ID]: 'name', [FIELD_CONFIG_KEYS.SHOULD_BE_DISABLED]: true }]);` * `cy.validateFormFooterButtons(buttonConfigs)` - validates form buttons based on provided configurations. `buttonConfigs` is an array of button configuration objects with properties: `buttonText` (required) - the text of the button, `buttonType` (optional, default: 'button') - the type of button (e.g., 'submit', 'reset'), `shouldBeDisabled` (optional, default: false) - whether the button should be disabled. e.g. `cy.validateFormFooterButtons([{ buttonText: 'Cancel' }, { buttonText: 'Submit', buttonType: 'submit', shouldBeDisabled: true }]);` or using constants: `cy.validateFormFooterButtons([{ [BUTTON_CONFIG_KEYS.TEXT]: 'Cancel' }]);` +##### provider_helper_commands + +* `cy.fillCommonFormFields(providerConfig, nameValue)` - fills common form fields that are present in all provider forms. `providerConfig` is the provider configuration object. `nameValue` is the name to use for the provider. +* `cy.fillFormFields(fields, values)` - fills form fields based on field definitions and values. `fields` is an array of field definition objects. `values` is an object containing field values. +* `cy.fillProviderForm(providerConfig, nameValue, hostValue)` - fills a provider form based on provider configuration. `providerConfig` is the provider configuration object. `nameValue` is the name to use for the provider. `hostValue` is the hostname to use for the provider. +* `cy.validateCommonFormFields(providerType, isEdit)` - validates common form fields that are present in all provider forms. `providerType` is the type of provider to be validated. `isEdit` is whether the form is in edit mode. +* `cy.validateFormFields(fields, isEdit)` - validates form fields based on field definitions. `fields` is an array of field definition objects. `isEdit` is whether the form is in edit mode. +* `cy.validateFormButtons(providerType, isEdit)` - validates form buttons (validate, add/save, reset, cancel). `providerType` is the type of provider to be validated. `isEdit` is whether the form is in edit mode. +* `cy.validateProviderForm(providerConfig, isEdit)` - validates a provider form based on provider configuration. `providerConfig` is the provider configuration object. `isEdit` is whether the form is in edit mode. +* `cy.updateProviderFieldsForEdit(providerConfig)` - updates provider fields for edit validation tests based on provider type. `providerConfig` is the provider configuration object. +* `cy.selectCreatedProvider(providerName)` - selects a created provider from the data table. `providerName` is the name of the provider to select. +* `cy.addProviderAndOpenEditForm(providerConfig, nameValue, hostValue)` - adds a provider and opens the edit form. `providerConfig` is the provider configuration object. `nameValue` is the name to use for the provider. `hostValue` is the hostname to use for the provider. +* `cy.interceptAddAzureStackProviderApi()` - intercepts the API call when adding an Azure Stack provider and forces a successful response. +* `cy.addAzureStackProviderAndOpenEditForm(providerConfig, nameValue, hostValue)` - special handling for Azure Stack provider which requires additional API interception. `providerConfig` is the provider configuration object. `nameValue` is the name to use for the provider. `hostValue` is the hostname to use for the provider. +* `cy.assertValidationFailureMessage()` - asserts validation failure message. +* `cy.assertValidationSuccessMessage()` - asserts validation success message. +* `cy.assertNameAlreadyExistsError()` - asserts name already exists error. +* `cy.validate({ stubErrorResponse, errorMessage })` - performs validation with optional error response stubbing. `stubErrorResponse` is whether to stub an error response. `errorMessage` is the error message to show. +* `cy.selectProviderAndDeleteWithOptionalFlashMessage({ createdProviderName, assertDeleteFlashMessage })` - deletes a provider with optional flash message check. `createdProviderName` is the name of the provider to delete. `assertDeleteFlashMessage` is whether to assert the delete flash message. +* `cy.cleanUp({ createdProviderName })` - cleans up a provider by deleting it. `createdProviderName` is the name of the provider to clean up. +* `generateProviderTests(providerConfig)` - generates all test suites for a provider. `providerConfig` is the provider configuration object. + #### Assertions * `cy.expect_explorer_title(title)` - check that the title on an explorer screen matches the provided title. `title`: String for the title. diff --git a/cypress/e2e/ui/Compute/Clouds/Providers/cloud_provider.cy.js b/cypress/e2e/ui/Compute/Clouds/Providers/cloud_provider.cy.js new file mode 100644 index 00000000000..9b879d45ff8 --- /dev/null +++ b/cypress/e2e/ui/Compute/Clouds/Providers/cloud_provider.cy.js @@ -0,0 +1,57 @@ +/* eslint-disable no-undef */ +import { generateProviderTests } from '../../../../../support/commands/provider_helper_commands'; +import { getProviderConfig, PROVIDER_TYPES } from './provider-factory'; + +describe('Automate Cloud Provider form operations: Compute > Clouds > Providers > Configuration > Add a New Cloud Provider', () => { + beforeEach(() => { + cy.login(); + cy.menu('Compute', 'Clouds', 'Providers'); + cy.toolbar('Configuration', 'Add a New Cloud Provider'); + }); + + // Generate tests for VMware vCloud provider + const vmwareVcloudConfig = getProviderConfig(PROVIDER_TYPES.VMWARE_VCLOUD); + generateProviderTests(vmwareVcloudConfig); + + // Generate tests for Amazon EC2 provider + const amazonEC2Config = getProviderConfig(PROVIDER_TYPES.AMAZON_EC2); + generateProviderTests(amazonEC2Config); + + // Generate tests for Azure provider + const azureConfig = getProviderConfig(PROVIDER_TYPES.AZURE); + generateProviderTests(azureConfig); + + // Generate tests for Azure Stack provider (requires special handling) + const azureStackConfig = getProviderConfig(PROVIDER_TYPES.AZURE_STACK); + generateProviderTests(azureStackConfig); + + // Generate tests for Google Compute Engine provider + const googleComputeConfig = getProviderConfig(PROVIDER_TYPES.GOOGLE_COMPUTE); + generateProviderTests(googleComputeConfig); + + // Generate tests for IBM Cloud VPC provider + const ibmCloudVpcConfig = getProviderConfig(PROVIDER_TYPES.IBM_CLOUD_VPC); + generateProviderTests(ibmCloudVpcConfig); + + // Generate tests for IBM Power Systems Virtual Servers provider + const ibmPowerSystemsConfig = getProviderConfig( + PROVIDER_TYPES.IBM_POWER_SYSTEMS + ); + generateProviderTests(ibmPowerSystemsConfig); + + // Generate tests for IBM PowerVC provider + const ibmPowerVcConfig = getProviderConfig(PROVIDER_TYPES.IBM_POWERVC); + generateProviderTests(ibmPowerVcConfig); + + // Generate tests for IBM Cloud Infrastructure Center provider + const ibmCicConfig = getProviderConfig(PROVIDER_TYPES.IBM_CIC); + generateProviderTests(ibmCicConfig); + + // Generate tests for Oracle Cloud provider + const oracleCloudConfig = getProviderConfig(PROVIDER_TYPES.ORACLE_CLOUD); + generateProviderTests(oracleCloudConfig); + + // Generate tests for OpenStack provider + const openstackConfig = getProviderConfig(PROVIDER_TYPES.OPENSTACK); + generateProviderTests(openstackConfig); +}); diff --git a/cypress/e2e/ui/Compute/Clouds/Providers/provider-factory.js b/cypress/e2e/ui/Compute/Clouds/Providers/provider-factory.js new file mode 100644 index 00000000000..550c6c770e1 --- /dev/null +++ b/cypress/e2e/ui/Compute/Clouds/Providers/provider-factory.js @@ -0,0 +1,1108 @@ +/** + * Provider Factory - Creates configuration objects for different cloud provider types + * This factory makes it easy to add new provider types with minimal code changes + */ + +// Common field labels +export const FIELD_LABELS = { + TYPE: 'Type', + NAME: 'Name', + ZONE: 'Zone', + API_VERSION: 'API Version', + HOSTNAME: 'Hostname (or IPv4 or IPv6 address)', + API_PORT: 'API Port', + USERNAME: 'Username', + PASSWORD: 'Password', + SECURITY_PROTOCOL: 'Security Protocol', + REGION: 'Region', + TENANT_ID: 'Tenant ID', + DOMAIN_ID: 'Domain ID', + USER_ID: 'User ID', + PUBLIC_KEY: 'Public Key', + PRIVATE_KEY: 'Private Key', + IBM_CLOUD: 'IBM Cloud', + SERVICE: 'Service', + PROJECT_ID: 'Project ID', + SUBSCRIPTION_ID: 'Subscription ID', + ENDPOINT_URL: 'Endpoint URL', + CLIENT_ID: 'Client ID', + CLIENT_KEY: 'Client Key', + ASSUME_ROLE_ARN: 'Assume role ARN', + ACCESS_KEY_ID: 'Access Key ID', + SECRET_ACCESS_KEY: 'Secret Access Key', + PROVIDER_REGION: 'Provider Region', + OPENSTACK_INFRA_PROVIDER: 'Openstack Infra Provider', + POWERVC_API_ENDPOINT: 'PowerVC API Endpoint (Hostname or IPv4/IPv6 address)', + ANSIBLE_ACCESS_METHOD: 'Ansible Access Method', + TENANT_MAPPING_ENABLED: 'Tenant Mapping Enabled', + API_USERNAME: 'API Username', + API_PASSWORD: 'API Password', + POWERVC: 'PowerVC', +}; + +// Common tab labels +export const TAB_LABELS = { + DEFAULT: 'Default', + EVENTS: 'Events', + METRICS: 'Metrics', + RSA_KEY_PAIR: 'RSA key pair', + IMAGE_EXPORT: 'Image Export', + SMARTSTATE_DOCKER: 'SmartState Docker', +}; + +// Common select options +export const SELECT_OPTIONS = { + EVENT_STREAM_TYPE_AMQP: 'AMQP', + EVENT_STREAM_TYPE_STF: 'STF', + ENABLED: 'Enabled', + API_VERSION_V3: 'Keystone V3', + API_VERSION_V5: 'vCloud API 5.5', + API_VERSION_V9: 'vCloud API 9.0', + API_VERSION_2017: 'V2017_03_09', + ZONE_DEFAULT: 'default', + SECURITY_PROTOCOL_SSL: 'SSL', + SECURITY_PROTOCOL_NON_SSL: 'Non-SSL', +}; + +// Common region options +export const REGION_OPTIONS = { + CENTRAL_INDIA: 'Central India', + CENTRAL_US: 'Central US', + HYDERABAD: 'ap-hyderabad-1', + MELBOURNE: 'ap-melbourne-1', + AUSTRALIA: 'Australia (Sydney)', + SPAIN: 'EU Spain (Madrid)', + CANADA: 'Canada (Central)', + ASIA_PACIFIC: 'Asia Pacific (Malaysia)', +}; + +// Common field values +export const FIELD_VALUES = { + TEST_NAME: 'Test Name:', + TENANT_ID: '101', + SUBSCRIPTION_ID: 'z565815f-05b6-402f-1999-045155da7dq4', + ENDPOINT_URL: '/api', + CLIENT_ID: 'manageiq.example.com', + CLIENT_KEY: 'test_client_key', + PORT: '3000', + USERNAME: 'admin@example.com', + PASSWORD: 'password123', + PRIVATE_KEY: + '-----BEGIN PRIVATE KEY-----\nMIIEvQIBAzApPugkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC\n-----END PRIVATE KEY-----', + PUBLIC_KEY: + '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAz14iAAOCAQ8AMIIBCgKCAQEA\n-----END PUBLIC KEY-----', + CLOUD_API_KEY: 'ibm_cloud_api_key_k#157', + GUID: '723e4a67-e89b-1qd3-z486-920614074000', + PROJECT_ID: 'gcp-project-123456', + ASSUME_ROLE: 'arn:aws:iam::123456789012:role/ManageIQRole', + ACCESS_KEY_ID: 'AKIAIOSFODNN7EXAMPLE', + SECRET_ACCESS_KEY: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + DOMAIN_ID_DEFAULT: 'default', + PROVIDER_REGION: 'RegionOne', +}; + +// Common flash message text snippets +export const FLASH_MESSAGES = { + OPERATION_CANCELLED: 'cancelled', + OPERATION_SAVED: 'saved', + DELETE_OPERATION: 'delete', + REFRESH_OPERATION: 'Refresh', + REMOVE_PROVIDER_BROWSER_ALERT: 'removed', +}; + +// Common validation messages +export const VALIDATION_MESSAGES = { + FAILED: 'Validation failed', + SUCCESSFUL: 'Validation successful', + NAME_ALREADY_EXISTS: 'already exists', +}; + +// Provider-specific validation error messages +export const VALIDATION_ERRORS = { + VMWARE_VCLOUD: 'Socket error: no address for example.manageiq.com', + ORACLE_CLOUD: 'The format of tenancy is invalid.', + IBM_CLOUD_VPC: 'Provided API key could not be found.', + IBM_POWER_SYSTEMS_VIRTUAL_SERVERS: 'IAM authentication failed', + GOOGLE_COMPUTE_ENGINE: 'Invalid Google JSON key', + AZURE_STACK: 'Failed to open TCP connection to example.manageiq.com:3000', + AZURE: 'Incorrect credentials - no host component for URI', + AMAZON_EC2: 'The security token included in the request is invalid.', + IBM_POWERVC: 'unable to retrieve IBM PowerVC release version number', + IBM_CIC: 'Login attempt timed out', + OPENSTACK: 'Socket error: no address for example.manageiq.com', +}; + +// Provider types +export const PROVIDER_TYPES = { + VMWARE_VCLOUD: 'VMware vCloud', + AZURE_STACK: 'Azure Stack', + IBM_CLOUD_VPC: 'IBM Cloud VPC', + IBM_POWER_SYSTEMS: 'IBM Power Systems Virtual Servers', + GOOGLE_COMPUTE: 'Google Compute Engine', + ORACLE_CLOUD: 'Oracle Cloud', + AZURE: 'Azure', + AMAZON_EC2: 'Amazon EC2', + IBM_POWERVC: 'IBM PowerVC', + IBM_CIC: 'IBM Cloud Infrastructure Center', + OPENSTACK: 'OpenStack', +}; + +/** + * Creates a provider configuration object with all necessary properties + * @param {string} type - The provider type + * @param {Object} config - Additional configuration options + * @returns {Object} - The provider configuration object + */ +function createProviderConfig(type, config = {}) { + const baseConfig = { + type, + nameValue: `${FIELD_VALUES.TEST_NAME} ${type}`, + validationError: VALIDATION_ERRORS[type.replace(/\s+/g, '_').toUpperCase()], + formFields: { + common: [ + { + label: FIELD_LABELS.TYPE, + id: 'type', + type: 'select', + required: true, + }, + { label: FIELD_LABELS.NAME, id: 'name', type: 'text', required: true }, + { + label: FIELD_LABELS.ZONE, + id: 'zone_id', + type: 'select', + required: true, + }, + ], + }, + formValues: { + common: { + type, + name: `${FIELD_VALUES.TEST_NAME} ${type}`, + zone_id: SELECT_OPTIONS.ZONE_DEFAULT, + }, + }, + fieldSelectionValues: {}, + }; + + // Merge with provided config + return { ...baseConfig, ...config }; +} + +/** + * Creates a VMware vCloud provider configuration + * @returns {Object} - The provider configuration + */ +function getVMwareVcloudProviderConfig() { + return createProviderConfig(PROVIDER_TYPES.VMWARE_VCLOUD, { + formFields: { + default: [ + { + label: FIELD_LABELS.API_VERSION, + id: 'api_version', + type: 'select', + required: true, + }, + { + label: FIELD_LABELS.HOSTNAME, + id: 'endpoints.default.hostname', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.API_PORT, + id: 'endpoints.default.port', + type: 'number', + required: true, + }, + { + label: FIELD_LABELS.USERNAME, + id: 'authentications.default.userid', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.PASSWORD, + id: 'authentications.default.password', + type: 'password', + required: true, + isPlaceholderInEditMode: true, + }, + ], + events: [ + { + label: FIELD_LABELS.TYPE, + id: 'event_stream_selection', + type: 'select', + required: false, + }, + { + label: FIELD_LABELS.SECURITY_PROTOCOL, + id: 'endpoints.amqp.security_protocol', + type: 'select', + required: false, + }, + { + label: FIELD_LABELS.HOSTNAME, + id: 'endpoints.amqp.hostname', + type: 'text', + required: false, + }, + { + label: FIELD_LABELS.API_PORT, + id: 'endpoints.amqp.port', + type: 'number', + required: false, + }, + { + label: FIELD_LABELS.USERNAME, + id: 'authentications.amqp.userid', + type: 'text', + required: false, + }, + { + label: FIELD_LABELS.PASSWORD, + id: 'authentications.amqp.password', + type: 'password', + required: false, + isPlaceholderInEditMode: true, + }, + ], + }, + formValues: { + default: { + api_version: SELECT_OPTIONS.API_VERSION_V5, + 'endpoints.default.port': FIELD_VALUES.PORT, + 'authentications.default.userid': FIELD_VALUES.USERNAME, + 'authentications.default.password': FIELD_VALUES.PASSWORD, + }, + }, + fieldSelectionValues: { + events: { + event_stream_selection: [SELECT_OPTIONS.EVENT_STREAM_TYPE_AMQP], + }, + }, + tabs: [TAB_LABELS.DEFAULT, TAB_LABELS.EVENTS], + }); +} + +/** + * Creates an Oracle Cloud provider configuration + * @returns {Object} - The provider configuration + */ +function getOracleCloudProviderConfig() { + return createProviderConfig(PROVIDER_TYPES.ORACLE_CLOUD, { + formFields: { + default: [ + { + label: FIELD_LABELS.REGION, + id: 'provider_region', + type: 'select', + required: true, + }, + { + label: FIELD_LABELS.TENANT_ID, + id: 'uid_ems', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.USER_ID, + id: 'authentications.default.userid', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.PRIVATE_KEY, + id: 'authentications.default.auth_key', + type: 'textarea', + required: true, + isPlaceholderInEditMode: true, + }, + { + label: FIELD_LABELS.PUBLIC_KEY, + id: 'authentications.default.public_key', + type: 'textarea', + required: true, + }, + ], + }, + formValues: { + default: { + provider_region: REGION_OPTIONS.HYDERABAD, + uid_ems: FIELD_VALUES.TENANT_ID, + 'authentications.default.userid': FIELD_VALUES.USERNAME, + 'authentications.default.auth_key': FIELD_VALUES.PRIVATE_KEY, + 'authentications.default.public_key': FIELD_VALUES.PUBLIC_KEY, + }, + }, + tabs: [TAB_LABELS.DEFAULT], + }); +} + +/** + * Creates an IBM Cloud VPC provider configuration + * @returns {Object} - The provider configuration + */ +function getIBMCloudVPCProviderConfig() { + return createProviderConfig(PROVIDER_TYPES.IBM_CLOUD_VPC, { + formFields: { + default: [ + { + label: FIELD_LABELS.REGION, + id: 'provider_region', + type: 'select', + required: true, + }, + { + label: FIELD_LABELS.IBM_CLOUD, + id: 'authentications.default.auth_key', + type: 'password', + required: true, + isPlaceholderInEditMode: true, + }, + ], + metrics: [ + { + label: FIELD_LABELS.TYPE, + id: 'metrics_selection', + type: 'select', + required: false, + }, + { + label: FIELD_LABELS.IBM_CLOUD, + id: 'endpoints.metrics.options.monitoring_instance_id', + type: 'password', + required: false, + }, + ], + events: [ + { + label: FIELD_LABELS.TYPE, + id: 'events_selection', + type: 'select', + required: false, + }, + { + label: FIELD_LABELS.IBM_CLOUD, + id: 'authentications.events.auth_key', + type: 'password', + required: false, + }, + ], + }, + formValues: { + default: { + provider_region: REGION_OPTIONS.AUSTRALIA, + 'authentications.default.auth_key': FIELD_VALUES.CLOUD_API_KEY, + }, + }, + fieldSelectionValues: { + metrics: { + metrics_selection: SELECT_OPTIONS.ENABLED, + }, + events: { + events_selection: SELECT_OPTIONS.ENABLED, + }, + }, + tabs: [TAB_LABELS.DEFAULT, TAB_LABELS.METRICS, TAB_LABELS.EVENTS], + }); +} + +/** + * Creates an IBM Power Systems Virtual Servers provider configuration + * @returns {Object} - The provider configuration + */ +function getIBMPowerSystemsProviderConfig() { + return createProviderConfig(PROVIDER_TYPES.IBM_POWER_SYSTEMS, { + formFields: { + default: [ + { + label: FIELD_LABELS.IBM_CLOUD, + id: 'authentications.default.auth_key', + type: 'password', + required: true, + isPlaceholderInEditMode: true, + }, + { + label: FIELD_LABELS.SERVICE, + id: 'uid_ems', + type: 'text', + required: true, + }, + ], + }, + formValues: { + default: { + 'authentications.default.auth_key': FIELD_VALUES.CLOUD_API_KEY, + uid_ems: FIELD_VALUES.GUID, + }, + }, + tabs: [TAB_LABELS.DEFAULT], + }); +} + +/** + * Creates a Google Compute Engine provider configuration + * @returns {Object} - The provider configuration + */ +function getGoogleComputeEngineProviderConfig() { + return createProviderConfig(PROVIDER_TYPES.GOOGLE_COMPUTE, { + formFields: { + default: [ + { + label: FIELD_LABELS.PROJECT_ID, + id: 'project', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.SERVICE, + id: 'authentications.default.auth_key', + type: 'textarea', + required: true, + isPlaceholderInEditMode: true, + }, + ], + }, + formValues: { + default: { + project: FIELD_VALUES.PROJECT_ID, + 'authentications.default.auth_key': `{"type":"service_account","project_id":"${FIELD_VALUES.PROJECT_ID}","private_key":"${FIELD_VALUES.PRIVATE_KEY}"}`, + }, + }, + tabs: [TAB_LABELS.DEFAULT], + }); +} + +/** + * Creates an Azure Stack provider configuration + * @returns {Object} - The provider configuration + */ +function getAzureStackProviderConfig() { + return createProviderConfig(PROVIDER_TYPES.AZURE_STACK, { + formFields: { + default: [ + { + label: FIELD_LABELS.TENANT_ID, + id: 'uid_ems', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.SUBSCRIPTION_ID, + id: 'subscription', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.API_VERSION, + id: 'api_version', + type: 'select', + required: true, + }, + { + label: FIELD_LABELS.SECURITY_PROTOCOL, + id: 'endpoints.default.security_protocol', + type: 'select', + required: true, + }, + { + label: FIELD_LABELS.HOSTNAME, + id: 'endpoints.default.hostname', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.API_PORT, + id: 'endpoints.default.port', + type: 'number', + required: true, + }, + { + label: FIELD_LABELS.USERNAME, + id: 'authentications.default.userid', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.PASSWORD, + id: 'authentications.default.password', + type: 'password', + required: true, + isPlaceholderInEditMode: true, + }, + ], + }, + formValues: { + default: { + uid_ems: FIELD_VALUES.TENANT_ID, + subscription: FIELD_VALUES.SUBSCRIPTION_ID, + api_version: SELECT_OPTIONS.API_VERSION_2017, + 'endpoints.default.security_protocol': + SELECT_OPTIONS.SECURITY_PROTOCOL_SSL, + 'endpoints.default.port': FIELD_VALUES.PORT, + 'authentications.default.userid': FIELD_VALUES.USERNAME, + 'authentications.default.password': FIELD_VALUES.PASSWORD, + }, + }, + tabs: [TAB_LABELS.DEFAULT], + }); +} + +/** + * Creates an Azure provider configuration + * @returns {Object} - The provider configuration + */ +function getAzureProviderConfig() { + return createProviderConfig(PROVIDER_TYPES.AZURE, { + formFields: { + default: [ + { + label: FIELD_LABELS.REGION, + id: 'provider_region', + type: 'select', + required: true, + }, + { + label: FIELD_LABELS.TENANT_ID, + id: 'uid_ems', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.SUBSCRIPTION_ID, + id: 'subscription', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.ENDPOINT_URL, + id: 'endpoints.default.url', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.CLIENT_ID, + id: 'authentications.default.userid', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.CLIENT_KEY, + id: 'authentications.default.password', + type: 'password', + required: true, + isPlaceholderInEditMode: true, + }, + ], + }, + formValues: { + default: { + provider_region: REGION_OPTIONS.CENTRAL_INDIA, + uid_ems: FIELD_VALUES.TENANT_ID, + subscription: FIELD_VALUES.SUBSCRIPTION_ID, + 'endpoints.default.url': FIELD_VALUES.ENDPOINT_URL, + 'authentications.default.userid': FIELD_VALUES.CLIENT_ID, + 'authentications.default.password': FIELD_VALUES.CLIENT_KEY, + }, + }, + tabs: [TAB_LABELS.DEFAULT], + }); +} + +/** + * Creates an Amazon EC2 provider configuration + * @returns {Object} - The provider configuration + */ +function getAmazonEC2ProviderConfig() { + return createProviderConfig(PROVIDER_TYPES.AMAZON_EC2, { + formFields: { + default: [ + { + label: FIELD_LABELS.REGION, + id: 'provider_region', + type: 'select', + required: true, + }, + { + label: FIELD_LABELS.ENDPOINT_URL, + id: 'endpoints.default.url', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.ASSUME_ROLE_ARN, + id: 'authentications.default.service_account', + type: 'text', + required: false, + }, + { + label: FIELD_LABELS.ACCESS_KEY_ID, + id: 'authentications.default.userid', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.SECRET_ACCESS_KEY, + id: 'authentications.default.password', + type: 'password', + required: true, + isPlaceholderInEditMode: true, + }, + ], + smartstate_docker: [ + { + label: FIELD_LABELS.USERNAME, + id: 'authentications.smartstate_docker.userid', + type: 'text', + required: false, + }, + { + label: FIELD_LABELS.PASSWORD, + id: 'authentications.smartstate_docker.password', + type: 'password', + required: false, + isPlaceholderInEditMode: true, + }, + ], + }, + formValues: { + default: { + provider_region: REGION_OPTIONS.CANADA, + 'endpoints.default.url': FIELD_VALUES.ENDPOINT_URL, + 'authentications.default.service_account': FIELD_VALUES.ASSUME_ROLE, + 'authentications.default.userid': FIELD_VALUES.ACCESS_KEY_ID, + 'authentications.default.password': FIELD_VALUES.SECRET_ACCESS_KEY, + }, + }, + tabs: [TAB_LABELS.DEFAULT, TAB_LABELS.SMARTSTATE_DOCKER], + }); +} + +/** + * Creates an IBM PowerVC provider configuration + * @returns {Object} - The provider configuration + */ +function getIBMPowerVCProviderConfig() { + return createProviderConfig(PROVIDER_TYPES.IBM_POWERVC, { + formFields: { + default: [ + { + label: FIELD_LABELS.PROVIDER_REGION, + id: 'provider_region', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.TENANT_MAPPING_ENABLED, + id: 'tenant_mapping_enabled', + type: 'checkbox', + required: false, + }, + { + label: FIELD_LABELS.SECURITY_PROTOCOL, + id: 'endpoints.default.security_protocol', + type: 'select', + required: true, + }, + { + label: FIELD_LABELS.POWERVC_API_ENDPOINT, + id: 'endpoints.default.hostname', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.API_PORT, + id: 'endpoints.default.port', + type: 'number', + required: true, + }, + { + label: FIELD_LABELS.API_USERNAME, + id: 'authentications.default.userid', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.API_PASSWORD, + id: 'authentications.default.password', + type: 'password', + required: true, + isPlaceholderInEditMode: true, + }, + ], + events: [ + { + label: FIELD_LABELS.TYPE, + id: 'event_stream_selection', + type: 'select', + required: false, + }, + ], + rsa_key_pair: [ + { + label: FIELD_LABELS.USERNAME, + id: 'authentications.ssh_keypair.userid', + type: 'text', + required: false, + }, + { + label: FIELD_LABELS.PRIVATE_KEY, + id: 'authentications.ssh_keypair.auth_key', + type: 'textarea', + required: false, + isPlaceholderInEditMode: true, + }, + ], + image_export: [ + { + label: FIELD_LABELS.POWERVC, + id: 'authentications.node.userid', + type: 'text', + required: false, + }, + { + label: FIELD_LABELS.ANSIBLE_ACCESS_METHOD, + id: 'authentications.node.options', + type: 'select', + required: false, + }, + { + label: FIELD_LABELS.POWERVC, + id: 'authentications.node.auth_key_password', + type: 'password', + required: false, + }, + { + label: FIELD_LABELS.POWERVC, + id: 'authentications.node.auth_key', + type: 'textarea', + required: false, + }, + ], + }, + formValues: { + default: { + uid_ems: FIELD_VALUES.DOMAIN_ID_DEFAULT, + 'endpoints.default.security_protocol': + SELECT_OPTIONS.SECURITY_PROTOCOL_SSL, + 'endpoints.default.port': FIELD_VALUES.PORT, + 'authentications.default.userid': FIELD_VALUES.USERNAME, + 'authentications.default.password': FIELD_VALUES.PASSWORD, + }, + }, + tabs: [ + TAB_LABELS.DEFAULT, + TAB_LABELS.EVENTS, + TAB_LABELS.RSA_KEY_PAIR, + TAB_LABELS.IMAGE_EXPORT, + ], + }); +} + +/** + * Creates an IBM Cloud Infrastructure Center provider configuration + * @returns {Object} - The provider configuration + */ +function getIBMCICProviderConfig() { + return createProviderConfig(PROVIDER_TYPES.IBM_CIC, { + formFields: { + default: [ + { + label: FIELD_LABELS.PROVIDER_REGION, + id: 'provider_region', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.API_VERSION, + id: 'api_version', + type: 'select', + required: true, + }, + { + label: FIELD_LABELS.DOMAIN_ID, + id: 'uid_ems', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.TENANT_MAPPING_ENABLED, + id: 'tenant_mapping_enabled', + type: 'checkbox', + required: false, + }, + { + label: FIELD_LABELS.SECURITY_PROTOCOL, + id: 'endpoints.default.security_protocol', + type: 'select', + required: true, + }, + { + label: FIELD_LABELS.HOSTNAME, + id: 'endpoints.default.hostname', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.API_PORT, + id: 'endpoints.default.port', + type: 'number', + required: true, + }, + { + label: FIELD_LABELS.USERNAME, + id: 'authentications.default.userid', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.PASSWORD, + id: 'authentications.default.password', + type: 'password', + required: true, + isPlaceholderInEditMode: true, + }, + { + label: FIELD_LABELS.TYPE, + id: 'event_stream_selection', + type: 'select', + required: false, + }, + { + label: FIELD_LABELS.HOSTNAME, + id: 'endpoints.amqp.hostname', + type: 'text', + required: false, + }, + { + label: FIELD_LABELS.API_PORT, + id: 'endpoints.amqp.port', + type: 'number', + required: false, + }, + { + label: FIELD_LABELS.USERNAME, + id: 'authentications.amqp.userid', + type: 'text', + required: false, + }, + { + label: FIELD_LABELS.PASSWORD, + id: 'authentications.amqp.password', + type: 'password', + required: false, + }, + { + label: FIELD_LABELS.USERNAME, + id: 'authentications.ssh_keypair.userid', + type: 'text', + required: false, + }, + { + label: FIELD_LABELS.PRIVATE_KEY, + id: 'authentications.ssh_keypair.auth_key', + type: 'textarea', + required: false, + isPlaceholderInEditMode: true, + }, + ], + }, + formValues: { + default: { + provider_region: FIELD_VALUES.PROVIDER_REGION, + api_version: SELECT_OPTIONS.API_VERSION_V3, + uid_ems: FIELD_VALUES.DOMAIN_ID_DEFAULT, + 'endpoints.default.security_protocol': + SELECT_OPTIONS.SECURITY_PROTOCOL_SSL, + 'endpoints.default.port': FIELD_VALUES.PORT, + 'authentications.default.userid': FIELD_VALUES.USERNAME, + 'authentications.default.password': FIELD_VALUES.PASSWORD, + }, + }, + // Values used for field selection that conditionally show other fields + fieldSelectionValues: { + default: { + api_version: SELECT_OPTIONS.API_VERSION_V3, + // TODO: set up multiple value validation + event_stream_selection: [ + SELECT_OPTIONS.EVENT_STREAM_TYPE_AMQP, + SELECT_OPTIONS.EVENT_STREAM_TYPE_STF, + ], + }, + }, + tabs: [TAB_LABELS.DEFAULT], + }); +} + +/** + * Creates an OpenStack provider configuration + * @returns {Object} - The provider configuration + */ +function getOpenStackProviderConfig() { + return createProviderConfig(PROVIDER_TYPES.OPENSTACK, { + formFields: { + default: [ + { + label: FIELD_LABELS.PROVIDER_REGION, + id: 'provider_region', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.OPENSTACK_INFRA_PROVIDER, + id: 'provider_id', + type: 'text', + required: false, + }, + { + label: FIELD_LABELS.API_VERSION, + id: 'api_version', + type: 'select', + required: true, + }, + { + label: FIELD_LABELS.DOMAIN_ID, + id: 'uid_ems', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.TENANT_MAPPING_ENABLED, + id: 'tenant_mapping_enabled', + type: 'checkbox', + required: false, + }, + { + label: FIELD_LABELS.SECURITY_PROTOCOL, + id: 'endpoints.default.security_protocol', + type: 'select', + required: true, + }, + { + label: FIELD_LABELS.HOSTNAME, + id: 'endpoints.default.hostname', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.API_PORT, + id: 'endpoints.default.port', + type: 'number', + required: true, + }, + { + label: FIELD_LABELS.USERNAME, + id: 'authentications.default.userid', + type: 'text', + required: true, + }, + { + label: FIELD_LABELS.PASSWORD, + id: 'authentications.default.password', + type: 'password', + required: true, + isPlaceholderInEditMode: true, + }, + { + label: FIELD_LABELS.TYPE, + id: 'event_stream_selection', + type: 'select', + required: false, + }, + { + label: FIELD_LABELS.HOSTNAME, + id: 'endpoints.amqp.hostname', + type: 'text', + required: false, + }, + { + label: FIELD_LABELS.API_PORT, + id: 'endpoints.amqp.port', + type: 'number', + required: false, + }, + { + label: FIELD_LABELS.USERNAME, + id: 'authentications.amqp.userid', + type: 'text', + required: false, + }, + { + label: FIELD_LABELS.PASSWORD, + id: 'authentications.amqp.password', + type: 'password', + required: false, + }, + { + label: FIELD_LABELS.USERNAME, + id: 'authentications.ssh_keypair.userid', + type: 'text', + required: false, + }, + { + label: FIELD_LABELS.PRIVATE_KEY, + id: 'authentications.ssh_keypair.auth_key', + type: 'textarea', + required: false, + isPlaceholderInEditMode: true, + }, + ], + }, + formValues: { + default: { + provider_region: FIELD_VALUES.PROVIDER_REGION, + api_version: SELECT_OPTIONS.API_VERSION_V3, + uid_ems: FIELD_VALUES.DOMAIN_ID_DEFAULT, + 'endpoints.default.security_protocol': + SELECT_OPTIONS.SECURITY_PROTOCOL_SSL, + 'endpoints.default.port': FIELD_VALUES.PORT, + 'authentications.default.userid': FIELD_VALUES.USERNAME, + 'authentications.default.password': FIELD_VALUES.PASSWORD, + }, + }, + // Values used for field selection that conditionally show other fields + fieldSelectionValues: { + default: { + api_version: SELECT_OPTIONS.API_VERSION_V3, + // TODO: set up multiple value validation + event_stream_selection: [ + SELECT_OPTIONS.EVENT_STREAM_TYPE_AMQP, + SELECT_OPTIONS.EVENT_STREAM_TYPE_STF, + ], + }, + }, + tabs: [TAB_LABELS.DEFAULT], + }); +} + +/** + * Provider Registry - Maps provider types to their factory functions + * This makes it easy to get a provider configuration by type + */ +const PROVIDER_REGISTRY = { + [PROVIDER_TYPES.VMWARE_VCLOUD]: getVMwareVcloudProviderConfig, + [PROVIDER_TYPES.ORACLE_CLOUD]: getOracleCloudProviderConfig, + [PROVIDER_TYPES.IBM_CLOUD_VPC]: getIBMCloudVPCProviderConfig, + [PROVIDER_TYPES.IBM_POWER_SYSTEMS]: getIBMPowerSystemsProviderConfig, + [PROVIDER_TYPES.GOOGLE_COMPUTE]: getGoogleComputeEngineProviderConfig, + [PROVIDER_TYPES.AZURE_STACK]: getAzureStackProviderConfig, + [PROVIDER_TYPES.AZURE]: getAzureProviderConfig, + [PROVIDER_TYPES.AMAZON_EC2]: getAmazonEC2ProviderConfig, + [PROVIDER_TYPES.IBM_POWERVC]: getIBMPowerVCProviderConfig, + [PROVIDER_TYPES.IBM_CIC]: getIBMCICProviderConfig, + [PROVIDER_TYPES.OPENSTACK]: getOpenStackProviderConfig, +}; + +/** + * Gets a provider configuration by type + * @param {string} type - The provider type + * @returns {Object} - The provider configuration + */ +export function getProviderConfig(type) { + if (!PROVIDER_REGISTRY[type]) { + cy.logAndThrowError(`Provider type "${type}" is not supported`); + } + return PROVIDER_REGISTRY[type](); +} diff --git a/cypress/support/commands/provider_helper_commands.js b/cypress/support/commands/provider_helper_commands.js new file mode 100644 index 00000000000..7bc62e13f4a --- /dev/null +++ b/cypress/support/commands/provider_helper_commands.js @@ -0,0 +1,936 @@ +import { + SELECT_OPTIONS, + TAB_LABELS, + FIELD_LABELS, + VALIDATION_MESSAGES, + PROVIDER_TYPES, + REGION_OPTIONS, + FLASH_MESSAGES, +} from '../../e2e/ui/Compute/Clouds/Providers/provider-factory'; +import { flashClassMap } from '../assertions/assertion_constants'; + +/** + * Generates a unique identifier using timestamp + * @returns {string} + */ +function generateUniqueIdentifier() { + return new Date().getTime(); +} + +/** + * Transforms any text string into a slug format using the given seperator('-', '_') + * @param {*} text + * @param {*} separator + * @example slugifyWith("Cloud Provider Name", "_") + * will return "cloud_provider_name" + */ +function slugifyWith(text, separator) { + return text.toLowerCase().replace(/\s+/g, separator); +} + +/** + * Fills common form fields that are present in all provider forms + * @param {Object} providerConfig - The provider configuration object + * @param {string} nameValue - The name to use for the provider + */ +Cypress.Commands.add('fillCommonFormFields', (providerConfig, nameValue) => { + cy.getFormSelectFieldById({ selectId: 'type' }).select(providerConfig.type); + cy.getFormInputFieldByIdAndType({ inputId: 'name' }).type(nameValue); + cy.getFormSelectFieldById({ selectId: 'zone_id' }).select( + SELECT_OPTIONS.ZONE_DEFAULT + ); +}); + +/** + * Fills form fields based on field definitions and values + * @param {Array} fields - Array of field definition objects + * @param {Object} values - Object containing field values + */ +Cypress.Commands.add('fillFormFields', (fields, values) => { + fields.forEach((field) => { + if (!values[field.id]) return; + + switch (field.type) { + case 'select': + cy.getFormSelectFieldById({ selectId: field.id }).select( + values[field.id] + ); + break; + case 'text': + cy.getFormInputFieldByIdAndType({ inputId: field.id }).type( + values[field.id] + ); + break; + case 'number': + cy.getFormInputFieldByIdAndType({ + inputId: field.id, + inputType: 'number', + }) + .clear() + .type(values[field.id]); + break; + case 'password': + cy.getFormInputFieldByIdAndType({ + inputId: field.id, + inputType: 'password', + }).type(values[field.id]); + break; + case 'textarea': + cy.getFormTextareaById({ textareaId: field.id }).type(values[field.id]); + break; + case 'checkbox': + if (values[field.id]) { + cy.getFormInputFieldByIdAndType({ + inputId: field.id, + inputType: 'checkbox', + }).check(); + } + break; + default: + break; + } + }); +}); + +/** + * Fills a provider form based on provider configuration + * @param {Object} providerConfig - The provider configuration object + * @param {string} nameValue - The name to use for the provider + * @param {string} hostValue - The hostname to use for the provider + */ +Cypress.Commands.add( + 'fillProviderForm', + (providerConfig, nameValue, hostValue) => { + cy.fillCommonFormFields(providerConfig, nameValue); + const defaultTabKey = slugifyWith(TAB_LABELS.DEFAULT, '_'); + const defaultTabFormFields = providerConfig.formFields[defaultTabKey]; + const defaultTabFormValues = providerConfig.formValues[defaultTabKey]; + if (defaultTabFormFields && defaultTabFormValues) { + cy.fillFormFields(defaultTabFormFields, defaultTabFormValues); + } + + // Since the host value needs to be unique per test, it's passed as a parameter instead of + // being hardcoded in the static formValues object. Only the default field is used(endpoints.default.hostname) + if (hostValue) { + const defaultHostFieldId = 'endpoints.default.hostname'; + const defaultHostFieldExists = providerConfig.formFields[ + defaultTabKey + ].find((fieldObject) => fieldObject.id === defaultHostFieldId); + if (defaultHostFieldExists) { + cy.getFormInputFieldByIdAndType({ + inputId: defaultHostFieldId, + }).type(hostValue); + } + } + } +); + +/** + * Validates common form fields that are present in all provider forms + * @param {boolean} isEdit - Whether the form is in edit mode + */ +Cypress.Commands.add('validateCommonFormFields', (providerType, isEdit) => { + cy.getFormLabelByForAttribute({ forValue: 'type' }) + .should('be.visible') + .and('contain.text', FIELD_LABELS.TYPE); + if (isEdit) { + cy.getFormSelectFieldById({ selectId: 'type' }) + .should('be.visible') + .and('be.disabled') + .find('option:selected') + .should('have.text', providerType); + } else { + cy.getFormSelectFieldById({ selectId: 'type' }) + .should('be.visible') + .and('be.enabled') + .select(providerType); + } + cy.getFormLabelByForAttribute({ forValue: 'name' }) + .should('be.visible') + .and('contain.text', FIELD_LABELS.NAME); + cy.getFormInputFieldByIdAndType({ inputId: 'name' }) + .should('be.visible') + .and('be.enabled'); + cy.getFormLabelByForAttribute({ forValue: 'zone_id' }) + .should('be.visible') + .and('contain.text', FIELD_LABELS.ZONE); + cy.getFormSelectFieldById({ selectId: 'zone_id' }) + .should('be.visible') + .and('be.enabled'); +}); + +/** + * Validates form fields based on field definitions + * @param {Array} fields - Array of field definition objects + * @param {boolean} isEdit - Whether the form is in edit mode + */ +Cypress.Commands.add('validateFormFields', (fields, isEdit) => { + fields.forEach((field) => { + // Skip label validation for placeholder fields in edit mode + if (!(isEdit && field.isPlaceholderInEditMode)) { + cy.getFormLabelByForAttribute({ forValue: field.id }) + .scrollIntoView() + .should('be.visible') + .and('contain.text', field.label); + } + + switch (field.type) { + case 'select': + cy.getFormSelectFieldById({ selectId: field.id }) + .should('be.visible') + .and('be.enabled'); + break; + case 'text': + cy.getFormInputFieldByIdAndType({ inputId: field.id }) + .should('be.visible') + .and('be.enabled'); + break; + case 'number': + cy.getFormInputFieldByIdAndType({ + inputId: field.id, + inputType: 'number', + }) + .should('be.visible') + .and('be.enabled'); + break; + case 'password': + // Password fields appear as disabled placeholders in edit mode + // with a legend instead of a label, and have a different ID format + if (isEdit && field.isPlaceholderInEditMode) { + cy.contains('.bx--fieldset legend.bx--label', field.label); + cy.getFormInputFieldByIdAndType({ + inputId: `${field.id}-password-placeholder`, + inputType: 'password', + }) + .should('be.visible') + .and('be.disabled'); + } else { + cy.getFormInputFieldByIdAndType({ + inputId: field.id, + inputType: 'password', + }) + .should('be.visible') + .and('be.enabled'); + } + break; + case 'textarea': + // Similar to password fields, Auth key fields appear as disabled + // placeholders with a legend instead of a label + if (isEdit && field.isPlaceholderInEditMode) { + cy.contains('.bx--fieldset legend.bx--label', field.label); + cy.getFormInputFieldByIdAndType({ + inputId: `${field.id}-password-placeholder`, + inputType: 'password', + }) + .should('be.visible') + .and('be.disabled'); + } else { + cy.getFormTextareaById({ textareaId: field.id }) + .should('be.visible') + .and('be.enabled'); + } + break; + case 'checkbox': + cy.getFormInputFieldByIdAndType({ + inputId: field.id, + inputType: 'checkbox', + }) + .should('be.visible') + .and('be.enabled'); + break; + default: + break; + } + }); +}); + +/** + * Validates form buttons (validate, add/save, reset, cancel) + * @param {string} providerType - Type of provider to be validated + * @param {boolean} isEdit - Whether the form is in edit mode + */ +Cypress.Commands.add('validateFormButtons', (providerType, isEdit) => { + cy.contains('button', 'Validate') + .scrollIntoView() + .should('be.visible') + .and('be.disabled'); + // TODO: Confirm why the Save button for "IBM Cloud VPC" is enabled in edit mode even when no changes are made + const saveButtonAssertionText = + isEdit && providerType === PROVIDER_TYPES.IBM_CLOUD_VPC + ? 'be.enabled' + : 'be.disabled'; + cy.getFormFooterButtonByTypeWithText({ + buttonText: isEdit ? 'Save' : 'Add', + buttonType: 'submit', + }) + .should('be.visible') + .and(saveButtonAssertionText); + if (isEdit) { + const resetButtonEnabledProviderTypes = [ + PROVIDER_TYPES.VMWARE_VCLOUD, + PROVIDER_TYPES.IBM_CLOUD_VPC, + PROVIDER_TYPES.IBM_CIC, + PROVIDER_TYPES.OPENSTACK, + ]; + const resetButtonAssertionText = resetButtonEnabledProviderTypes.includes( + providerType + ) + ? 'be.enabled' + : 'be.disabled'; + cy.getFormFooterButtonByTypeWithText({ buttonText: 'Reset' }) + .should('be.visible') + .and(resetButtonAssertionText); + } + cy.getFormFooterButtonByTypeWithText({ buttonText: 'Cancel' }) + .should('be.visible') + .and('be.enabled'); +}); + +/** + * Handles special tab setup requirements before validation + * Some tabs require selecting specific values in dropdowns before other fields become visible + * @param {string} tab - The tab name + * @param {Object} providerConfig - The provider configuration + */ +function setupTabForValidation(tab, providerConfig) { + if (tab === TAB_LABELS.DEFAULT) { + if (providerConfig.fieldSelectionValues.default) { + const eventStreamValues = + providerConfig.fieldSelectionValues.default.event_stream_selection; + cy.getFormSelectFieldById({ + selectId: 'api_version', + }).select(providerConfig.fieldSelectionValues.default.api_version); + cy.getFormSelectFieldById({ + selectId: 'event_stream_selection', + // TODO: Enhance to sequentially select values and validate their associated + // fields before moving to the next(like in IBM CIC & Openstack) + }).select(eventStreamValues[0]); + } + } else if (tab === TAB_LABELS.EVENTS) { + if (providerConfig.fieldSelectionValues.events) { + if (providerConfig.fieldSelectionValues.events.event_stream_selection) { + const eventStreamValues = + providerConfig.fieldSelectionValues.events.event_stream_selection; + cy.getFormSelectFieldById({ + selectId: 'event_stream_selection', + // TODO: Enhance to sequentially select values and validate their + // associated fields, if necessary in events tab + }).select(eventStreamValues[0]); + } else if (providerConfig.fieldSelectionValues.events.events_selection) { + cy.getFormSelectFieldById({ + selectId: 'events_selection', + }).select(providerConfig.fieldSelectionValues.events.events_selection); + } + } + } else if (tab === TAB_LABELS.METRICS) { + // For IBM Cloud VPC and other providers with metrics_selection + if (providerConfig.fieldSelectionValues.metrics) { + if (providerConfig.fieldSelectionValues.metrics.metrics_selection) { + cy.getFormSelectFieldById({ selectId: 'metrics_selection' }).select( + providerConfig.fieldSelectionValues.metrics.metrics_selection + ); + } + } + } +} + +/** + * Validates a provider form based on provider configuration + * @param {Object} providerConfig - The provider configuration object + * @param {boolean} isEdit - Whether the form is in edit mode + */ +Cypress.Commands.add('validateProviderForm', (providerConfig, isEdit) => { + cy.validateCommonFormFields(providerConfig.type, isEdit); + providerConfig.tabs.forEach((tab) => { + // If not on the default tab, switch to it + if (tab !== TAB_LABELS.DEFAULT) { + cy.tabs({ tabLabel: tab }); + } + // Set up the tab for validation (select required dropdown values) + setupTabForValidation(tab, providerConfig); + const tabKeyCorrespondingToTabName = slugifyWith(tab, '_'); + if (providerConfig.formFields[tabKeyCorrespondingToTabName]) { + cy.validateFormFields( + providerConfig.formFields[tabKeyCorrespondingToTabName], + isEdit + ); + } + }); + // Switch back to default tab + if (providerConfig.tabs.length > 1) { + cy.tabs({ tabLabel: TAB_LABELS.DEFAULT }); + } + cy.validateFormButtons(providerConfig.type, isEdit); +}); + +/** + * Updates provider fields for edit validation tests based on provider type + * @param {Object} providerType - The provider type + */ +Cypress.Commands.add('updateProviderFieldsForEdit', (providerType) => { + // Different providers need different fields updated for validation + switch (providerType) { + case PROVIDER_TYPES.VMWARE_VCLOUD: + cy.getFormSelectFieldById({ selectId: 'api_version' }).select( + SELECT_OPTIONS.API_VERSION_V9 + ); + break; + case PROVIDER_TYPES.ORACLE_CLOUD: + cy.getFormSelectFieldById({ selectId: 'provider_region' }).select( + REGION_OPTIONS.MELBOURNE + ); + break; + case PROVIDER_TYPES.IBM_CLOUD_VPC: + cy.getFormSelectFieldById({ selectId: 'provider_region' }).select( + REGION_OPTIONS.SPAIN + ); + break; + case PROVIDER_TYPES.IBM_POWER_SYSTEMS: + cy.getFormInputFieldByIdAndType({ inputId: 'uid_ems' }).type('-xr4q'); + break; + case PROVIDER_TYPES.GOOGLE_COMPUTE: + cy.getFormInputFieldByIdAndType({ inputId: 'project' }).type('-76g1'); + break; + case PROVIDER_TYPES.AZURE_STACK: + case PROVIDER_TYPES.IBM_POWERVC: + case PROVIDER_TYPES.IBM_CIC: + case PROVIDER_TYPES.OPENSTACK: + cy.getFormSelectFieldById({ + selectId: 'endpoints.default.security_protocol', + }).select(SELECT_OPTIONS.SECURITY_PROTOCOL_NON_SSL); + break; + case PROVIDER_TYPES.AZURE: + cy.getFormSelectFieldById({ selectId: 'provider_region' }).select( + REGION_OPTIONS.CENTRAL_US + ); + break; + case PROVIDER_TYPES.AMAZON_EC2: + cy.getFormSelectFieldById({ selectId: 'provider_region' }).select( + REGION_OPTIONS.ASIA_PACIFIC + ); + break; + default: + } +}); + +/** + * Selects a created provider from the data table + * @param {string} providerName - The name of the provider to select + */ +Cypress.Commands.add('selectCreatedProvider', (providerName) => { + // Set pagination to 200 items per page to include the target provider despite pending deletions + cy.get( + '.miq-fieldset-content .miq-pagination select#bx-pagination-select-1' + ).select('200'); + cy.selectTableRowsByText({ textArray: [providerName] }); +}); + +/** + * Adds a provider and opens the edit form + * @param {Object} providerConfig - The provider configuration object + * @param {string} nameValue - The name to use for the provider + * @param {string} hostValue - The hostname to use for the provider (optional) + */ +Cypress.Commands.add( + 'addProviderAndOpenEditForm', + (providerConfig, nameValue, hostValue) => { + cy.fillProviderForm(providerConfig, nameValue, hostValue); + cy.validate({ + stubErrorResponse: false, + }); + cy.getFormFooterButtonByTypeWithText({ + buttonText: 'Add', + buttonType: 'submit', + }).click(); + cy.selectCreatedProvider(nameValue); + cy.toolbar('Configuration', 'Edit Selected Cloud Provider'); + } +); + +/** + * Intercepts the API call when adding an Azure Stack provider and forces a successful response + * + * This command intercepts the POST request to '/api/providers' that occurs when adding an Azure Stack + * provider. It allows the request to reach the server (so data is created) but then overrides the + * response status code to 200 (OK). This is necessary because the server would normally return a 500 + * error when using mock values for fields like host, port, etc. in test environments. + * + * The command automatically triggers the form submission by clicking the 'Add' button. + */ +Cypress.Commands.add('interceptAddAzureStackProviderApi', () => { + cy.interceptApi({ + alias: 'addAzureStackProviderApi', + urlPattern: '/api/providers', + triggerFn: () => + cy + .getFormFooterButtonByTypeWithText({ + buttonText: 'Add', + buttonType: 'submit', + }) + .click(), + responseInterceptor: (req) => { + // Let the request go through to the server(so that the data is created) and then override + // the response statusCode to 200, server will return internal_server_error(500) since we are + // using mock values for fields like host, port, etc. + req.continue((res) => { + res.send(200); + }); + }, + }); +}); + +/** + * Special handling for Azure Stack provider which requires additional API interception + * @param {Object} providerConfig - The provider configuration object + * @param {string} nameValue - The name to use for the provider + * @param {string} hostValue - The hostname to use for the provider + */ +Cypress.Commands.add( + 'addAzureStackProviderAndOpenEditForm', + (providerConfig, nameValue, hostValue) => { + cy.fillProviderForm(providerConfig, nameValue, hostValue); + cy.validate({ + stubErrorResponse: false, + }); + cy.interceptAddAzureStackProviderApi(); + cy.selectCreatedProvider(nameValue); + // TODO: Switch to cy.interceptApi once its enhanced to support multiple api intercepts from a single event + cy.intercept( + 'GET', + /\/api\/providers\/(\d+)\?attributes=endpoints,authentications/, + (req) => { + const providerId = req.url.match(/\/api\/providers\/(\d+)\?/)[1]; + req.continue((res) => { + res.send(200, { + id: providerId, + type: 'ManageIQ::Providers::AzureStack::CloudManager', + name: nameValue, + zone_id: '2', + uid_ems: providerConfig.formValues.default.uid_ems, + subscription: providerConfig.formValues.default.subscription, + api_version: providerConfig.formValues.default.api_version, + endpoints: [ + { + role: 'default', + hostname: hostValue, + port: providerConfig.formValues.default[ + 'endpoints.default.port' + ], + security_protocol: + providerConfig.formValues.default[ + 'endpoints.default.security_protocol' + ], + }, + ], + authentications: [ + { + authtype: 'default', + userid: + providerConfig.formValues.default[ + 'authentications.default.userid' + ], + }, + ], + }); + }); + } + ).as('getProviderFieldValuesApi'); + cy.intercept( + 'GET', + '/api/zones?expand=resources&attributes=id,name,visible&filter[]=visible!=false&sort_by=name', + (req) => { + req.continue((res) => { + res.send(200, { + name: 'zones', + count: 2, + subcount: 1, + subquery_count: 1, + pages: 1, + resources: [ + { + href: 'http://localhost:3000/api/zones/2', + id: '2', + name: SELECT_OPTIONS.ZONE_DEFAULT, + visible: true, + }, + ], + }); + }); + } + ).as('getZoneDropdownOptionsApi'); + cy.toolbar('Configuration', 'Edit Selected Cloud Provider'); + cy.wait('@getProviderFieldValuesApi'); + cy.wait('@getZoneDropdownOptionsApi'); + } +); + +/** + * Asserts validation failure message + */ +Cypress.Commands.add('assertValidationFailureMessage', () => { + cy.contains('.ddorg__carbon-error-helper-text', VALIDATION_MESSAGES.FAILED); +}); + +/** + * Asserts validation success message + */ +Cypress.Commands.add('assertValidationSuccessMessage', () => { + cy.contains('.bx--form__helper-text', VALIDATION_MESSAGES.SUCCESSFUL); +}); + +/** + * Asserts name already exists error + */ +Cypress.Commands.add('assertNameAlreadyExistsError', () => { + cy.contains('#name-error-msg', VALIDATION_MESSAGES.NAME_ALREADY_EXISTS); +}); + +/** + * Performs validation with optional error response stubbing + * @param {boolean} stubErrorResponse - Whether to stub an error response + * @param {string} errorMessage - The error message to show + */ +Cypress.Commands.add('validate', ({ stubErrorResponse, errorMessage }) => { + let response = { state: 'Finished', status: 'Error' }; + if (stubErrorResponse) { + response = { + ...response, + message: errorMessage, + }; + } else { + response = { ...response, status: 'Ok', task_results: {} }; + } + // not using cy.interceptApi because each validate call requires a fresh alias registration, + // reusing the same intercept callback results in it returning the first response object + cy.intercept('GET', '/api/tasks/*?attributes=task_results', response).as( + 'validateApi' + ); + cy.contains('button', 'Validate').click(); + cy.wait('@validateApi'); +}); + +/** + * Deletes a provider with optional flash message check + * @param {string} createdProviderName - The name of the provider to delete + * @param {boolean} assertDeleteFlashMessage - Whether to assert the delete flash message + */ +Cypress.Commands.add( + 'selectProviderAndDeleteWithOptionalFlashMessage', + ({ createdProviderName, assertDeleteFlashMessage }) => { + cy.selectCreatedProvider(createdProviderName); + cy.interceptApi({ + alias: 'deleteProviderApi', + urlPattern: '/ems_cloud/button?pressed=ems_cloud_delete', + triggerFn: () => + cy.expect_browser_confirm_with_text({ + confirmTriggerFn: () => + cy.toolbar( + 'Configuration', + 'Remove Cloud Providers from Inventory' + ), + containsText: 'removed', + }), + onApiResponse: () => { + if (assertDeleteFlashMessage) { + cy.expect_flash(flashClassMap.success, 'delete'); + } + }, + }); + } +); + +/** + * Cleans up a provider by deleting it + * @param {string} createdProviderName - The name of the provider to clean up + */ +Cypress.Commands.add('cleanUp', ({ createdProviderName }) => { + cy.url() + .then((url) => { + // Navigate to cloud providers table view + if (!url.endsWith('/ems_cloud/show_list#/')) { + cy.visit('/ems_cloud/show_list#/'); + } + }) + .then(() => { + cy.selectProviderAndDeleteWithOptionalFlashMessage({ + createdProviderName, + assertDeleteFlashMessage: false, + }); + }); +}); + +/** + * Provider Test Generator helpers - Generates test cases for cloud providers + * These utilities make it easy to create test cases for different provider types + */ + +/** + * Generates a test suite for validating the add form of a provider + * @param {Object} providerConfig - The provider configuration object + */ +function generateAddFormValidationTests(providerConfig, isAzureStack = false) { + describe(`Validate ${providerConfig.type} add form`, () => { + it('Validate visibility of elements', () => { + cy.validateProviderForm(providerConfig, false); + }); + + it('Should show the error message from the task_results API upon validation failure and validate cancel button behavior', () => { + cy.fillProviderForm( + providerConfig, + providerConfig.nameValue, + 'manageiq.example.com' + ); + cy.validate({ + stubErrorResponse: true, + errorMessage: providerConfig.validationError, + }); + cy.assertValidationFailureMessage(); + cy.getFormFooterButtonByTypeWithText({ buttonText: 'Cancel' }).click(); + cy.expect_flash( + flashClassMap.success, + FLASH_MESSAGES.OPERATION_CANCELLED + ); + }); + + it('Verify successful validate + add/refresh/delete operations', () => { + /** + * The provider name is set in this variable to identify it for deletion + */ + const uniqueId = generateUniqueIdentifier(); + const nameValue = `${providerConfig.nameValue} - verify-validate-add-refresh-and-delete-operations - ${uniqueId}`; + const hostValue = `${slugifyWith( + providerConfig.type, + '-' + )}-${uniqueId}.com`; + cy.fillProviderForm(providerConfig, nameValue, hostValue); + //Add + cy.validate({ + stubErrorResponse: false, + }); + cy.assertValidationSuccessMessage(); + // Azure Stack needs to be handled differently, add similar cases if needed + if (isAzureStack) { + cy.interceptAddAzureStackProviderApi(); + } else { + cy.getFormFooterButtonByTypeWithText({ + buttonText: 'Add', + buttonType: 'submit', + }) + .should('be.enabled') + .click(); + } + cy.expect_flash(flashClassMap.success, FLASH_MESSAGES.OPERATION_SAVED); + // Refresh + cy.selectCreatedProvider(nameValue); + cy.expect_browser_confirm_with_text({ + confirmTriggerFn: () => + cy.toolbar('Configuration', 'Refresh Relationships and Power States'), + containsText: FLASH_MESSAGES.REFRESH_OPERATION, + }); + cy.expect_flash(flashClassMap.success, FLASH_MESSAGES.REFRESH_OPERATION); + // Delete + // FIXME: remove this block once bug is fixed + // Bug: After refresh, config option other than add remains disabled and requires any action to be performed to enable it back + /* ==================================================================== */ + cy.toolbar('Configuration', 'Add a New Cloud Provider'); + cy.getFormFooterButtonByTypeWithText({ buttonText: 'Cancel' }).click(); + /* ==================================================================== */ + cy.selectProviderAndDeleteWithOptionalFlashMessage({ + createdProviderName: nameValue, + assertDeleteFlashMessage: true, + }); + }); + }); +} + +/** + * Generates a test suite for validating the edit form of a provider + * @param {Object} providerConfig - The provider configuration object + * @param {boolean} isAzureStack - Whether the provider is Azure Stack (requires special handling) + */ +function generateEditFormValidationTests(providerConfig, isAzureStack = false) { + describe(`Validate ${providerConfig.type} edit form`, () => { + /** + * The provider name is set in this variable at the start of each test, + * allowing afterEach to identify it for deletion. + */ + let nameFieldValue; + let hostValue; + + it('Validate visibility of elements', () => { + const uniqueId = generateUniqueIdentifier(); + nameFieldValue = `${providerConfig.nameValue} - verify-edit-form-elements - ${uniqueId}`; + hostValue = `${slugifyWith(providerConfig.type, '-')}-${uniqueId}.com`; + // Azure Stack needs to be handled differently, add similar cases if needed + if (isAzureStack) { + cy.addAzureStackProviderAndOpenEditForm( + providerConfig, + nameFieldValue, + hostValue + ); + } else { + cy.addProviderAndOpenEditForm( + providerConfig, + nameFieldValue, + hostValue + ); + } + cy.validateProviderForm(providerConfig, true); + }); + + it("Should show the error message from the task_results API upon validation failure and validate reset & cancel buttons' behavior", () => { + const uniqueId = generateUniqueIdentifier(); + nameFieldValue = `${providerConfig.nameValue} - verify-edit-form-validation-error - ${uniqueId}`; + hostValue = `${slugifyWith(providerConfig.type, '-')}-${uniqueId}.com`; + // Azure Stack needs to be handled differently, add similar cases if needed + if (isAzureStack) { + cy.addAzureStackProviderAndOpenEditForm( + providerConfig, + nameFieldValue, + hostValue + ); + } else { + cy.addProviderAndOpenEditForm( + providerConfig, + nameFieldValue, + hostValue + ); + } + cy.updateProviderFieldsForEdit(providerConfig.type); + cy.validate({ + stubErrorResponse: true, + errorMessage: providerConfig.validationError, + }); + cy.assertValidationFailureMessage(); + cy.getFormFooterButtonByTypeWithText({ buttonText: 'Reset' }) + .should('be.enabled') + .click(); + cy.getFormFooterButtonByTypeWithText({ + buttonText: 'Cancel', + }).click(); + cy.expect_flash( + flashClassMap.success, + FLASH_MESSAGES.OPERATION_CANCELLED + ); + }); + + it('Verify successful validate + edit operation', () => { + const uniqueId = generateUniqueIdentifier(); + nameFieldValue = `${providerConfig.nameValue} - verify-validate-and-edit-operations - ${uniqueId}`; + hostValue = `${slugifyWith(providerConfig.type, '-')}-${uniqueId}.com`; + // Azure Stack needs to be handled differently, add similar cases if needed + if (isAzureStack) { + cy.addAzureStackProviderAndOpenEditForm( + providerConfig, + nameFieldValue, + hostValue + ); + } else { + cy.addProviderAndOpenEditForm( + providerConfig, + nameFieldValue, + hostValue + ); + } + // Update fields based on provider type + cy.updateProviderFieldsForEdit(providerConfig.type); + cy.validate({ + stubErrorResponse: false, + }); + cy.assertValidationSuccessMessage(); + // Azure Stack needs to be handled differently, add similar cases if needed + if (isAzureStack) { + cy.interceptApi({ + method: 'PATCH', + alias: 'editAzureStackProviderApi', + urlPattern: /\/api\/providers\/\d+/, + triggerFn: () => + cy + .getFormFooterButtonByTypeWithText({ + buttonText: 'Save', + buttonType: 'submit', + }) + .should('be.enabled') + .click(), + responseInterceptor: (req) => { + req.continue((res) => { + res.send(200); + }); + }, + }); + } else { + cy.getFormFooterButtonByTypeWithText({ + buttonText: 'Save', + buttonType: 'submit', + }) + .should('be.enabled') + .click(); + } + cy.expect_flash(flashClassMap.success, FLASH_MESSAGES.OPERATION_SAVED); + }); + + afterEach(() => { + cy.cleanUp({ createdProviderName: nameFieldValue }); + }); + }); +} + +/** + * Generates a test suite for validating the name uniqueness of a provider + * @param {Object} providerConfig - The provider configuration object + * @param {boolean} isAzureStack - Whether the provider is Azure Stack (requires special handling) + */ +function generateNameUniquenessTests(providerConfig, isAzureStack = false) { + describe(`${providerConfig.type} provider name uniqueness validation`, () => { + const uniqueId = generateUniqueIdentifier(); + /** + * The provider name is set in this variable at the start of the test, allowing afterEach to identify it for deletion. + */ + const nameFieldValue = `${providerConfig.nameValue} - verify-duplicate-restriction - ${uniqueId}`; + const hostValue = `${slugifyWith( + providerConfig.type, + '-' + )}-${uniqueId}.com`; + + beforeEach(() => { + cy.fillProviderForm(providerConfig, nameFieldValue, hostValue); + cy.validate({ + stubErrorResponse: false, + }); + // Azure Stack needs to be handled differently, add similar cases if needed + if (isAzureStack) { + cy.interceptAddAzureStackProviderApi(); + } else { + cy.getFormFooterButtonByTypeWithText({ + buttonText: 'Add', + buttonType: 'submit', + }).click(); + } + }); + + it('Should display error on duplicate name usage', () => { + cy.toolbar('Configuration', 'Add a New Cloud Provider'); + // Add same name as above + cy.fillProviderForm(providerConfig, nameFieldValue, hostValue); + cy.assertNameAlreadyExistsError(); + }); + + afterEach(() => { + cy.cleanUp({ createdProviderName: nameFieldValue }); + }); + }); +} + +/** + * Generates all test suites for a provider + * @param {Object} providerConfig - The provider configuration object + */ +export function generateProviderTests(providerConfig) { + const isAzureStack = providerConfig.type === PROVIDER_TYPES.AZURE_STACK; + + describe(`Validate cloud provider type: ${providerConfig.type}`, () => { + generateAddFormValidationTests(providerConfig, isAzureStack); + generateEditFormValidationTests(providerConfig, isAzureStack); + generateNameUniquenessTests(providerConfig, isAzureStack); + }); +} diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index ce300bfc921..948d54ecd24 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -50,6 +50,7 @@ import './commands/gtl.js'; import './commands/login.js'; import './commands/menu.js'; import './commands/miq_data_table_commands.js'; +import './commands/provider_helper_commands.js'; import './commands/select.js'; import './commands/stub_notifications.js'; import './commands/tabs.js';