Skip to content

Commit

Permalink
Enable Cypress tests to detect features of the OSD instance they are …
Browse files Browse the repository at this point in the history
…running against

Also:
* Turn on test isolation when security is disabled

Signed-off-by: Miki <[email protected]>
  • Loading branch information
AMoo-Miki committed Dec 20, 2024
1 parent bd91bbb commit eb357a8
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 15 deletions.
11 changes: 8 additions & 3 deletions cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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!',
Expand All @@ -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',
Expand All @@ -42,10 +45,10 @@ module.exports = defineConfig({
},
});

function setupNodeEvents(
async function setupNodeEvents(
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
): Cypress.PluginConfigOptions {
): Promise<Cypress.PluginConfigOptions> {
if (process.env.COVERAGE) {
config.env.codeCoverage = { url: '/__coverage__' };
codeCoverageTask(on, config);
Expand Down Expand Up @@ -75,5 +78,7 @@ function setupNodeEvents(
})
);

await setupDynamicConfig(config);

return config;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
20 changes: 9 additions & 11 deletions cypress/integration/dashboard_sanity_test.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,14 @@ 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(() => {});

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));
Expand All @@ -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', () => {
Expand All @@ -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(() => {});
Expand Down
100 changes: 100 additions & 0 deletions cypress/scripts/dynamic_config.ts
Original file line number Diff line number Diff line change
@@ -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);
};
105 changes: 105 additions & 0 deletions cypress/support/conditional_describers.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions cypress/support/e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit eb357a8

Please sign in to comment.