diff --git a/.github/workflows/cypress-workflow.yml b/.github/workflows/cypress-workflow.yml new file mode 100644 index 000000000..67d2e3c46 --- /dev/null +++ b/.github/workflows/cypress-workflow.yml @@ -0,0 +1,95 @@ +name: Cypress integration tests workflow +on: + pull_request: + branches: + - "*" + push: + branches: + - "*" +env: + OPENSEARCH_DASHBOARDS_VERSION: '2.4' + OPENSEARCH_VERSION: '2.4.0-SNAPSHOT' +jobs: + tests: + name: Run Cypress E2E tests + runs-on: ubuntu-latest + env: + # prevents extra Cypress installation progress messages + CI: 1 + # avoid warnings like "tput: No value for $TERM and no -T specified" + TERM: xterm + steps: + - name: Set up JDK + uses: actions/setup-java@v1 + with: + # TODO: Parse this from security analytics plugin + java-version: 11 + - name: Checkout index management + uses: actions/checkout@v2 + with: + path: security-analytics + repository: opensearch-project/security-analytics + ref: '2.4' + - name: Run opensearch with plugin + run: | + cd security-analytics + ./gradlew run -Dopensearch.version=${{ env.OPENSEARCH_VERSION }} & + sleep 300 + # timeout 300 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:9200)" != "200" ]]; do sleep 5; done' + - name: Checkout Security Analytics Dashboards plugin + uses: actions/checkout@v2 + with: + path: security-analytics-dashboards-plugin + - name: Checkout OpenSearch-Dashboards + uses: actions/checkout@v2 + with: + repository: opensearch-project/OpenSearch-Dashboards + path: OpenSearch-Dashboards + ref: ${{ env.OPENSEARCH_DASHBOARDS_VERSION }} + - name: Get node and yarn versions + id: versions + run: | + echo "::set-output name=node_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.node).match(/[.0-9]+/)[0]")" + echo "::set-output name=yarn_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.yarn).match(/[.0-9]+/)[0]")" + - name: Setup node + uses: actions/setup-node@v1 + with: + node-version: ${{ steps.versions.outputs.node_version }} + registry-url: 'https://registry.npmjs.org' + - name: Install correct yarn version for OpenSearch-Dashboards + run: | + npm uninstall -g yarn + echo "Installing yarn ${{ steps.versions_step.outputs.yarn_version }}" + npm i -g yarn@${{ steps.versions.outputs.yarn_version }} + - name: Bootstrap plugin/OpenSearch-Dashboards + run: | + mkdir -p OpenSearch-Dashboards/plugins + mv security-analytics-dashboards-plugin OpenSearch-Dashboards/plugins + cd OpenSearch-Dashboards/plugins/security-analytics-dashboards-plugin + yarn osd bootstrap + - name: Run OpenSearch-Dashboards server + run: | + cd OpenSearch-Dashboards + yarn start --no-base-path --no-watch & + sleep 300 + # timeout 300 bash -c 'while [[ "$(curl -s localhost:5601/api/status | jq -r '.status.overall.state')" != "green" ]]; do sleep 5; done' + # for now just chrome, use matrix to do all browsers later + - name: Cypress tests + uses: cypress-io/github-action@v2 + with: + working-directory: OpenSearch-Dashboards/plugins/security-analytics-dashboards-plugin + command: yarn run cypress run + wait-on: 'http://localhost:5601' + browser: chrome + # Screenshots are only captured on failure, will change this once we do visual regression tests + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: cypress-screenshots + path: OpenSearch-Dashboards/plugins/security-analytics-dashboards-plugin/cypress/screenshots + # Test run video was always captured, so this action uses "always()" condition + - uses: actions/upload-artifact@v1 + if: always() + with: + name: cypress-videos + path: OpenSearch-Dashboards/plugins/security-analytics-dashboards-plugin/cypress/videos diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts new file mode 100644 index 000000000..eee870220 --- /dev/null +++ b/cypress/plugins/index.ts @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +// TODO: yarn osd bootstrap fails when trying to add below package as a dependency.. +// const wp = require("@cypress/webpack-preprocessor"); +// +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on) => { + // const options = { + // webpackOptions: { + // resolve: { + // extensions: [".ts", ".tsx", ".js", ".jsx", ".json"], + // }, + // module: { + // rules: [ + // { + // test: /\.tsx?$/, + // loader: "ts-loader", + // options: { transpileOnly: true }, + // }, + // ], + // }, + // }, + // }; + // + // on("file:preprocessor", wp(options)); +}; diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 000000000..11d1938b8 --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,115 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +const { ADMIN_AUTH, INDICES, NODE_API, PLUGIN_NAME } = require('./constants'); + +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +Cypress.Commands.overwrite('visit', (originalFn, url, options) => { + // Add the basic auth header when security enabled in the Opensearch cluster + // https://github.com/cypress-io/cypress/issues/1288 + if (Cypress.env('security_enabled')) { + const ADMIN_AUTH = { + username: Cypress.env('username'), + password: Cypress.env('password'), + }; + if (options) { + options.auth = ADMIN_AUTH; + } else { + options = { auth: ADMIN_AUTH }; + } + // Add query parameters - select the default OSD tenant + options.qs = { security_tenant: 'private' }; + return originalFn(url, options); + } else { + return originalFn(url, options); + } +}); + +// Be able to add default options to cy.request(), https://github.com/cypress-io/cypress/issues/726 +Cypress.Commands.overwrite('request', (originalFn, ...args) => { + let defaults = {}; + // Add the basic authentication header when security enabled in the Opensearch cluster + const ADMIN_AUTH = { + username: Cypress.env('username'), + password: Cypress.env('password'), + }; + if (Cypress.env('security_enabled')) { + defaults.auth = ADMIN_AUTH; + } + + let options = {}; + if (typeof args[0] === 'object' && args[0] !== null) { + options = Object.assign({}, args[0]); + } else if (args.length === 1) { + [options.url] = args; + } else if (args.length === 2) { + [options.method, options.url] = args; + } else if (args.length === 3) { + [options.method, options.url, options.body] = args; + } + + return originalFn(Object.assign({}, defaults, options)); +}); + +Cypress.Commands.add('deleteAllIndices', () => { + cy.request('DELETE', `${Cypress.env('opensearch')}/index*,sample*,opensearch_dashboards*,test*`); +}); + +Cypress.Commands.add('createDetector', (detectorJSON) => { + cy.request('POST', `${Cypress.env('opensearch')}${NODE_API.DETECTORS_BASE}`, detectorJSON); +}); + +Cypress.Commands.add('updateDetector', (detectorId, detectorJSON) => { + cy.request( + 'PUT', + `${Cypress.env('opensearch')}/${NODE_API.DETECTORS_BASE}/${detectorId}`, + detectorJSON + ); +}); + +Cypress.Commands.add('createRule', (ruleJSON) => { + cy.request('POST', `${Cypress.env('opensearch')}${NODE_API.RULES_BASE}`, ruleJSON); +}); + +Cypress.Commands.add('updateRule', (ruleId, ruleJSON) => { + cy.request('PUT', `${Cypress.env('opensearch')}/${NODE_API.RULES_BASE}/${ruleId}`, ruleJSON); +}); + +Cypress.Commands.add('createIndex', (index, settings = {}) => { + cy.request('PUT', `${Cypress.env('opensearch')}/${index}`, settings); +}); + +Cypress.Commands.add('createIndexTemplate', (name, template) => { + cy.request( + 'PUT', + `${Cypress.env('opensearch')}${NODE_API.INDEX_TEMPLATE_BASE}/${name}`, + template + ); +}); diff --git a/cypress/support/constants.js b/cypress/support/constants.js new file mode 100644 index 000000000..d6f34163a --- /dev/null +++ b/cypress/support/constants.js @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { API } from '../../server/utils/constants'; + +export const TWENTY_SECONDS = 20000; + +export const INDICES = { + DETECTORS_INDEX: '.opensearch-detectors-config', + PRE_PACKAGED_RULES_INDEX: '.opensearch-pre-packaged-rules-config', + CUSTOM_RULES_INDEX: '.opensearch-custom-rules-config', +}; + +export const PLUGIN_NAME = 'opensearch_index_management_dashboards'; + +export const NODE_API = { + ...API, + INDEX_TEMPLATE_BASE: '/_index_template', +}; diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts new file mode 100644 index 000000000..16182a154 --- /dev/null +++ b/cypress/support/index.d.ts @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/// + +declare namespace Cypress { + interface Chainable { + /** + * Deletes all indices in cluster + * @example + * cy.deleteAllIndices() + */ + deleteAllIndices(): Chainable; + + /** + * Creates a detector + * @example + * cy.createPolicy({ "detector_type": ... }) + */ + createDetector(detectorJSON: object): Chainable; + + /** + * Updates settings for index + * @example + * cy.updateIndexSettings("some_index", settings) + */ + updateDetector(detectorId: string, detectorJSON: object): Chainable; + + /** + * Creates index with policy + * @example + * cy.createIndex("some_index", "some_policy") + */ + createIndex(index: string, settings?: object): Chainable; + + /** + * Creates an index template. + * @example + * cy.createIndexTemplate("some_index_template", { "index_patterns": "abc", "properties": { ... } }) + */ + createIndexTemplate(name: string, template: object): Chainable; + } +} diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 000000000..cf15f85bb --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,33 @@ +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/; +Cypress.on('uncaught:exception', (err) => { + /* returning false here prevents Cypress from failing the test */ + if (resizeObserverLoopErrRe.test(err.message)) { + return false; + } +}); + +// Switch the base URL of Opensearch when security enabled in the cluster +// Not doing this for Dashboards because it can still use http when security enabled +if (Cypress.env('security_enabled')) { + Cypress.env('opensearch', `https://${Cypress.env('opensearch_url')}`); +} else { + Cypress.env('opensearch', `http://${Cypress.env('opensearch_url')}`); +}