From eb357a8ac1ce0d708340a53820a1c589f31b39fd Mon Sep 17 00:00:00 2001 From: Miki Date: Wed, 18 Dec 2024 16:17:37 -0800 Subject: [PATCH] Enable Cypress tests to detect features of the OSD instance they are running against Also: * Turn on test isolation when security is disabled Signed-off-by: Miki --- cypress.config.ts | 11 +- .../apps/query_enhancements/a_check.spec.js | 2 +- .../integration/dashboard_sanity_test.spec.js | 20 ++-- cypress/scripts/dynamic_config.ts | 100 +++++++++++++++++ cypress/support/conditional_describers.js | 105 ++++++++++++++++++ cypress/support/e2e.js | 2 + 6 files changed, 225 insertions(+), 15 deletions(-) create mode 100644 cypress/scripts/dynamic_config.ts create mode 100644 cypress/support/conditional_describers.js diff --git a/cypress.config.ts b/cypress.config.ts index 4a58fde09fbd..ca28f54e8bde 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -6,6 +6,7 @@ import { defineConfig } from 'cypress'; import codeCoverageTask from '@cypress/code-coverage/task'; import webpackPreprocessor from '@cypress/webpack-preprocessor'; +import { setupDynamicConfig } from './cypress/scripts/dynamic_config'; module.exports = defineConfig({ defaultCommandTimeout: 60000, @@ -23,7 +24,6 @@ module.exports = defineConfig({ url: 'http://localhost:9200', }, openSearchUrl: 'http://localhost:9200', - SECURITY_ENABLED: false, AGGREGATION_VIEW: false, username: 'admin', password: 'myStrongPassword123!', @@ -33,6 +33,9 @@ module.exports = defineConfig({ DATASOURCE_MANAGEMENT_ENABLED: false, ML_COMMONS_DASHBOARDS_ENABLED: true, WAIT_FOR_LOADER_BUFFER_MS: 0, + + // This value is automatically determined at runtime + SECURITY_ENABLED: false, }, e2e: { baseUrl: 'http://localhost:5601', @@ -42,10 +45,10 @@ module.exports = defineConfig({ }, }); -function setupNodeEvents( +async function setupNodeEvents( on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions -): Cypress.PluginConfigOptions { +): Promise { if (process.env.COVERAGE) { config.env.codeCoverage = { url: '/__coverage__' }; codeCoverageTask(on, config); @@ -75,5 +78,7 @@ function setupNodeEvents( }) ); + await setupDynamicConfig(config); + return config; } diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/a_check.spec.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/a_check.spec.js index c2eca9265aa9..3462c83aff00 100644 --- a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/a_check.spec.js +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/a_check.spec.js @@ -9,7 +9,7 @@ import { SECONDARY_ENGINE } from '../../../../../utils/constants'; const miscUtils = new MiscUtils(cy); -describe('No Index Pattern Check Test', () => { +ifEnabled('WORKSPACE').describe('No Index Pattern Check Test', () => { before(() => { // Load test data cy.setupTestData( diff --git a/cypress/integration/dashboard_sanity_test.spec.js b/cypress/integration/dashboard_sanity_test.spec.js index 98877f9c6dbb..217a2d0fe276 100644 --- a/cypress/integration/dashboard_sanity_test.spec.js +++ b/cypress/integration/dashboard_sanity_test.spec.js @@ -17,9 +17,6 @@ import { export function dashboardSanityTests() { const commonUI = new CommonUI(cy); const miscUtils = new MiscUtils(cy); - const baseURL = new URL(Cypress.config().baseUrl); - // remove trailing slash - const path = baseURL.pathname.replace(/\/$/, ''); describe('dashboard sample data validation', () => { before(() => {}); @@ -27,7 +24,7 @@ export function dashboardSanityTests() { after(() => {}); describe('checking home page', () => { - before(() => { + beforeEach(() => { // Go to the home page miscUtils.visitPage('app/home#'); cy.window().then((win) => win.localStorage.setItem('home:welcome:show', false)); @@ -40,35 +37,35 @@ export function dashboardSanityTests() { it('checking opensearch_dashboards_overview display', () => { // Check that opensearch_dashboards_overview is visable - commonUI.checkElementExists(`a[href="${path}/app/opensearch_dashboards_overview"]`, 1); + commonUI.checkElementExists(`a[href$="/app/opensearch_dashboards_overview"]`, 1); }); it('checking tutorial_directory display', () => { // Check that tutorial_directory is visable - commonUI.checkElementExists(`a[href="${path}/app/home#/tutorial_directory"]`, 2); + commonUI.checkElementExists(`a[href$="/app/home#/tutorial_directory"]`, 2); }); it('checking management display', () => { // Check that management is visable - commonUI.checkElementExists(`a[href="${path}/app/management"]`, 1); + commonUI.checkElementExists(`a[href$="/app/management"]`, 1); }); it('checking dev_tools display', () => { // Check that dev_tools is visable - commonUI.checkElementExists(`a[href="${path}/app/dev_tools#/console"]`, 2); + commonUI.checkElementExists(`a[href$="/app/dev_tools#/console"]`, 2); }); it('settings display', () => { // Check that settings is visable commonUI.checkElementExists( - `a[href="${path}/app/management/opensearch-dashboards/settings#defaultRoute"]`, + `a[href$="/app/management/opensearch-dashboards/settings#defaultRoute"]`, 1 ); }); it('checking feature_directory display', () => { // Check that feature_directory is visable - commonUI.checkElementExists(`a[href="${path}/app/home#/feature_directory"]`, 1); + commonUI.checkElementExists(`a[href$="/app/home#/feature_directory"]`, 1); }); it('checking navigation display', () => { @@ -83,9 +80,10 @@ export function dashboardSanityTests() { }); describe('checking Dev Tools', () => { - before(() => { + beforeEach(() => { // Go to the Dev Tools page miscUtils.visitPage('app/dev_tools#/console'); + cy.reload(true); }); after(() => {}); diff --git a/cypress/scripts/dynamic_config.ts b/cypress/scripts/dynamic_config.ts new file mode 100644 index 000000000000..a3ef5345e4c5 --- /dev/null +++ b/cypress/scripts/dynamic_config.ts @@ -0,0 +1,100 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable no-console */ + +import { setTimeout } from 'timers/promises'; +import fetch from 'node-fetch'; + +const CONNECTION_TIMEOUT = 15000; +const CONNECTION_TIMEOUT_TOTAL = 60000; +const CONNECTION_RETRY_INTERVAL = 15000; + +/** + * Check if the security plugin is enabled on Dashboards and set env.SECURITY_ENABLED accordingly. + * Turn on test isolation when security is disabled, and off when security is enabled so we don't + * have to log in for each test. + */ +const checkSecurity = async (config: Cypress.PluginConfigOptions) => { + const startTime = Date.now(); + do { + // Not catching to allow Cypress to fail + const resp = await fetch(config.baseUrl, { timeout: CONNECTION_TIMEOUT }); + + if (resp.status === 200) { + console.log('OpenSearch Dashboards is configured without security.'); + config.env.SECURITY_ENABLED = false; + + console.log('Test isolation is turned on.'); + config.testIsolation = true; + + return; + } + + if (resp.status === 401) { + console.log('OpenSearch Dashboards is configured with security.'); + config.env.SECURITY_ENABLED = true; + + console.log('Test isolation is turned off.'); + config.testIsolation = false; + + return; + } + + console.log('Waiting for OpenSearch Dashboards to be ready...'); + await setTimeout(CONNECTION_RETRY_INTERVAL); + } while (Date.now() - startTime < CONNECTION_TIMEOUT_TOTAL); + + throw new Error( + 'Security plugin status check failed: OpenSearch Dashboards unreachable or misconfigured.' + ); +}; + +const checkPlugins = async (config: Cypress.PluginConfigOptions) => { + const startTime = Date.now(); + const apiStatusUrl = new URL('/api/status', config.baseUrl); + const headers: fetch.HeadersInit = {}; + + if (config.env.SECURITY_ENABLED) { + headers.Authorization = + 'Basic ' + Buffer.from(config.env.username + ':' + config.env.password).toString('base64'); + } + + do { + // Not catching to allow Cypress to fail + const resp = await fetch(apiStatusUrl, { timeout: CONNECTION_TIMEOUT, headers }); + if (resp.status === 200) { + const json = await resp.json(); + + if (!Array.isArray(json?.status?.statuses)) + throw new Error( + 'Invalid OpenSearch Dashboards status response: OpenSearch Dashboards unreachable or misconfigured.' + ); + + json.status.statuses.forEach?.(({ id }: { id: string }) => { + if (!id.startsWith('plugin:')) return; + const envName = id + .replace(/^plugin:(.+?)(Dashboards)*@.*$/, '$1') + .replace(/([A-Z])/g, '_$1') + .toUpperCase(); + config.env[`${envName}_ENABLED`] = true; + }); + + return; + } + + console.log('Waiting for OpenSearch Dashboards to be ready...'); + await setTimeout(CONNECTION_RETRY_INTERVAL); + } while (Date.now() - startTime < CONNECTION_TIMEOUT_TOTAL); + + throw new Error( + 'Plugins status check failed: OpenSearch Dashboards unreachable or misconfigured.' + ); +}; + +export const setupDynamicConfig = async (config: Cypress.PluginConfigOptions) => { + await checkSecurity(config); + await checkPlugins(config); +}; diff --git a/cypress/support/conditional_describers.js b/cypress/support/conditional_describers.js new file mode 100644 index 000000000000..7efb1fbe0374 --- /dev/null +++ b/cypress/support/conditional_describers.js @@ -0,0 +1,105 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Helper function to describe conditional test suites, based on features enabled in OSD + * + * @param {string} featureName - The name of feature to be checked. + * Based on the status of the feature, the suite will be described. + * + * @returns {Mocha.SuiteFunction & { describe: Mocha.SuiteFunction, not: Mocha.SuiteFunction & { describe: Mocha.SuiteFunction }}} + */ + +const ifEnabled = (featureName) => { + /** + * Describes a "suite" that should be executed if the feature is enabled. + * @type {Mocha.SuiteFunction & { not: Mocha.SuiteFunction & {describe: Mocha.SuiteFunction}}} + */ + const describer = (name, fn, options = {}) => { + const { skip = false, only = false, condition = true, not = false } = options; + + if ( + skip || + !condition || + (!not && !Cypress.env(`${featureName}_ENABLED`)) || + (not && Cypress.env(`${featureName}_ENABLED`)) + ) { + describe.skip(name, fn); + } else if (only) { + // eslint-disable-next-line mocha/no-exclusive-tests + describe.only(name, fn); + } else { + describe(name, fn); + } + }; + + /** + * Describes a "suite" that should be executed if the feature is disabled. + * @type {Mocha.SuiteFunction & {describe: Mocha.SuiteFunction}} + */ + describer.not = (name, fn, options = {}) => { + describer(name, fn, { ...options, not: true }); + }; + + /** + * Describes a "suite" that should not be executed. + * @type {Mocha.PendingSuiteFunction} + */ + describer.skip = describer.not.skip = (name, fn) => { + describer(name, fn, { skip: true }); + }; + + /** + * Describes a "suite" that should be executed exclusively and only if the feature is enabled. + * @type {Mocha.ExclusiveSuiteFunction} + */ + describer.only = (name, fn) => { + describer(name, fn, { only: true }); + }; + + /** + * Describes a "suite" that should be executed exclusively and only if the feature is disabled. + * @type {Mocha.ExclusiveSuiteFunction} + */ + describer.not.only = (name, fn) => { + describer(name, fn, { not: true, only: true }); + }; + + // Ease-of-use aliases + + /** + * Describes a "suite" that should be executed if the feature is enabled. + * @type {Mocha.SuiteFunction} + */ + describer.describe = describer; + + /** + * Describes a "suite" that should be executed if the feature is disabled. + * @type {Mocha.SuiteFunction} + */ + describer.not.describe = describer.not; + + /** + * Describes a "suite" that should not be executed. + * @type {Mocha.PendingSuiteFunction} + */ + describer.describe.skip = describer.not.describe.skip = describer.skip; + + /** + * Describes a "suite" that should be executed exclusively and only if the feature is enabled. + * @type {Mocha.ExclusiveSuiteFunction} + */ + describer.only.describe = describer.only; + + /** + * Describes a "suite" that should be executed exclusively and only if the feature is disabled. + * @type {Mocha.ExclusiveSuiteFunction} + */ + describer.not.describe.only = describer.not.only; + + return describer; +}; + +global.ifEnabled = ifEnabled; diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 6a60ac3be661..7bbfc7f40e13 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -5,6 +5,8 @@ import '@cypress/code-coverage/support'; +import './conditional_describers'; + import '../utils/commands'; import '../utils/apps/commands'; import '../utils/dashboards/workspace-plugin/commands';