diff --git a/.evergreen/buildvariants-and-tasks.in.yml b/.evergreen/buildvariants-and-tasks.in.yml index 4a421254a0b..c59076373ad 100644 --- a/.evergreen/buildvariants-and-tasks.in.yml +++ b/.evergreen/buildvariants-and-tasks.in.yml @@ -275,6 +275,12 @@ buildvariants: <% } %> <% } %> + - name: test-web-sandbox-atlas-cloud + display_name: Test Web Sandbox (w/ Atlas Cloud login) + run_on: ubuntu2004-large + tasks: + - name: test-web-sandbox-atlas-cloud + - name: generate-vulnerability-report display_name: Vulnerability Report run_on: ubuntu2004-large @@ -504,6 +510,21 @@ tasks: <% } %> <% } %> + - name: test-web-sandbox-atlas-cloud + tags: + - required-for-publish + - run-on-pr + - assigned_to_jira_team_compass_compass + - foliage_check_task_only + commands: + - func: prepare + - func: install + - func: bootstrap + - func: test-web-sandbox-atlas-cloud + vars: + compass_distribution: compass + debug: 'compass-e2e-tests*,electron*,hadron*,mongo*' + - name: create_static_analysis_report tags: ['required-for-publish', 'run-on-pr'] depends_on: diff --git a/.evergreen/buildvariants-and-tasks.yml b/.evergreen/buildvariants-and-tasks.yml index a81034c3551..c0cf6dd6a32 100644 --- a/.evergreen/buildvariants-and-tasks.yml +++ b/.evergreen/buildvariants-and-tasks.yml @@ -257,6 +257,11 @@ buildvariants: - name: test-web-sandbox-firefox-1 - name: test-web-sandbox-firefox-2 - name: test-web-sandbox-firefox-3 + - name: test-web-sandbox-atlas-cloud + display_name: Test Web Sandbox (w/ Atlas Cloud login) + run_on: ubuntu2004-large + tasks: + - name: test-web-sandbox-atlas-cloud - name: generate-vulnerability-report display_name: Vulnerability Report run_on: ubuntu2004-large @@ -1715,6 +1720,20 @@ tasks: e2e_test_groups: 3 e2e_test_group: 3 debug: compass-e2e-tests*,electron*,hadron*,mongo* + - name: test-web-sandbox-atlas-cloud + tags: + - required-for-publish + - run-on-pr + - assigned_to_jira_team_compass_compass + - foliage_check_task_only + commands: + - func: prepare + - func: install + - func: bootstrap + - func: test-web-sandbox-atlas-cloud + vars: + compass_distribution: compass + debug: compass-e2e-tests*,electron*,hadron*,mongo* - name: create_static_analysis_report tags: - required-for-publish diff --git a/.evergreen/functions.yml b/.evergreen/functions.yml index 46204d5892b..b4dc14846d3 100644 --- a/.evergreen/functions.yml +++ b/.evergreen/functions.yml @@ -672,6 +672,42 @@ functions: eval $(.evergreen/print-compass-env.sh) npm run --unsafe-perm --workspace compass-e2e-tests test-ci web + + test-web-sandbox-atlas-cloud: + - command: shell.exec + # It can take a very long time for Atlas cluster to get deployed + timeout_secs: 2400 + params: + working_dir: src + shell: bash + env: + <<: *compass-env + DEBUG: ${debug|} + COMPASS_E2E_ATLAS_CLOUD_SANDBOX_CLOUD_CONFIG: 'qa' + COMPASS_E2E_ATLAS_CLOUD_SANDBOX_USERNAME: ${e2e_tests_compass_web_atlas_username} + COMPASS_E2E_ATLAS_CLOUD_SANDBOX_PASSWORD: ${e2e_tests_compass_web_atlas_password} + COMPASS_E2E_ATLAS_CLOUD_SANDBOX_DBUSER_USERNAME: ${e2e_tests_compass_web_atlas_db_username} + COMPASS_E2E_ATLAS_CLOUD_SANDBOX_DBUSER_PASSWORD: ${e2e_tests_compass_web_atlas_password} + MCLI_PUBLIC_API_KEY: ${e2e_tests_mcli_public_api_key} + MCLI_PRIVATE_API_KEY: ${e2e_tests_mcli_private_api_key} + MCLI_ORG_ID: ${e2e_tests_mcli_org_id} + MCLI_PROJECT_ID: ${e2e_tests_mcli_project_id} + MCLI_OPS_MANAGER_URL: ${e2e_tests_mcli_ops_manager_url} + script: | + set -e + # Load environment variables + eval $(.evergreen/print-compass-env.sh) + # Create Atlas cluster for test project + source .evergreen/start-atlas-cloud-cluster.sh + # Run the tests + echo "Starting e2e tests..." + # We're only running a special subset of tests as provisioning atlas + # clusters in CI is both pricey and flakey, so we want to limit the + # coverage to reduce those factors (at least for now) + npm run --unsafe-perm --workspace compass-e2e-tests test-ci -- -- web \ + --test-atlas-cloud-sandbox \ + --test-filter="atlas-cloud/**/*" + test-connectivity: - command: shell.exec # Fail the task if it's idle for 10 mins diff --git a/.evergreen/start-atlas-cloud-cluster.sh b/.evergreen/start-atlas-cloud-cluster.sh new file mode 100644 index 00000000000..63e87471639 --- /dev/null +++ b/.evergreen/start-atlas-cloud-cluster.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Atlas limits the naming to something like /^[\w\d-]{,23}$/ (and will auto +# truncate if it's too long) so we're very limited in terms of how unique this +# name can be. Hopefully the epoch + part of git hash is enough for these to not +# overlap when tests are running +ATLAS_CLOUD_TEST_CLUSTER_NAME="e2e-$(date +"%s")-$(git rev-parse HEAD)" + +function atlascli() { + docker run \ + -e MCLI_PUBLIC_API_KEY \ + -e MCLI_PRIVATE_API_KEY \ + -e MCLI_ORG_ID \ + -e MCLI_PROJECT_ID \ + -e MCLI_OPS_MANAGER_URL \ + mongodb/atlas atlas $@ +} + +cleanup() { + echo "Scheduling Atlas deployment \`$ATLAS_CLOUD_TEST_CLUSTER_NAME\` for deletion..." + atlascli clusters delete $ATLAS_CLOUD_TEST_CLUSTER_NAME --force +} + +trap cleanup EXIT + +echo "Creating Atlas deployment \`$ATLAS_CLOUD_TEST_CLUSTER_NAME\` to test against..." +atlascli clusters create $ATLAS_CLOUD_TEST_CLUSTER_NAME \ + --provider AWS \ + --region US_EAST_1 \ + --tier M10 + +echo "Waiting for the deployment to be provisioned..." +atlascli clusters watch "$ATLAS_CLOUD_TEST_CLUSTER_NAME" + +echo "Getting connection string for provisioned cluster..." +ATLAS_CLOUD_TEST_CLUSTER_CONNECTION_STRING_JSON="$(atlascli clusters connectionStrings describe $ATLAS_CLOUD_TEST_CLUSTER_NAME -o json)" + +export COMPASS_E2E_ATLAS_CLOUD_SANDBOX_DEFAULT_CONNECTIONS="{\"$ATLAS_CLOUD_TEST_CLUSTER_NAME\": $ATLAS_CLOUD_TEST_CLUSTER_CONNECTION_STRING_JSON}" +echo "Cluster connections: $COMPASS_E2E_ATLAS_CLOUD_SANDBOX_DEFAULT_CONNECTIONS" diff --git a/packages/compass-e2e-tests/helpers/commands/connect-form.ts b/packages/compass-e2e-tests/helpers/commands/connect-form.ts index fa1e52d5967..320f13443c2 100644 --- a/packages/compass-e2e-tests/helpers/commands/connect-form.ts +++ b/packages/compass-e2e-tests/helpers/commands/connect-form.ts @@ -7,6 +7,7 @@ import Debug from 'debug'; import { DEFAULT_CONNECTIONS, isTestingAtlasCloudExternal, + isTestingAtlasCloudSandbox, } from '../test-runner-context'; import { getConnectionTitle } from '@mongodb-js/connection-info'; const debug = Debug('compass-e2e-tests'); @@ -930,7 +931,7 @@ export async function saveConnection( export async function setupDefaultConnections(browser: CompassBrowser) { // When running tests against Atlas Cloud, connections can't be added or // removed from the UI manually, so we skip setup for default connections - if (isTestingAtlasCloudExternal()) { + if (isTestingAtlasCloudExternal() || isTestingAtlasCloudSandbox()) { return; } diff --git a/packages/compass-e2e-tests/helpers/commands/connect.ts b/packages/compass-e2e-tests/helpers/commands/connect.ts index 381ada2cf36..97356a91de5 100644 --- a/packages/compass-e2e-tests/helpers/commands/connect.ts +++ b/packages/compass-e2e-tests/helpers/commands/connect.ts @@ -1,13 +1,17 @@ import { - DEFAULT_CONNECTION_NAME_1, - DEFAULT_CONNECTION_NAME_2, DEFAULT_CONNECTION_STRING_1, + DEFAULT_CONNECTION_NAME_1, connectionNameFromString, } from '../compass'; import type { CompassBrowser } from '../compass-browser'; import type { ConnectFormState } from '../connect-form-state'; import * as Selectors from '../selectors'; import Debug from 'debug'; +import { + DEFAULT_CONNECTION_NAMES, + isTestingAtlasCloudExternal, + isTestingAtlasCloudSandbox, +} from '../test-runner-context'; const debug = Debug('compass-e2e-tests'); @@ -35,14 +39,27 @@ type ConnectOptions = ConnectionResultOptions & { removeConnections?: boolean; }; +/** + * Use this command when you need to add a new connection with a specific + * connection string. Most test files should just be using + * browser.connectToDefaults() + */ export async function connectWithConnectionString( browser: CompassBrowser, - connectionString = DEFAULT_CONNECTION_STRING_1, + connectionStringOrName?: string, options: ConnectOptions = {} ): Promise { - // Use this command when you need to add a new connection with a specific - // connection string. Most test files should just be using - // browser.connectToDefaults() + // When testing Atlas Cloud, we can't really create a new connection, so just + // assume a connection name was passed (with a fallback to a default one) and + // try to use it + if (isTestingAtlasCloudExternal() || isTestingAtlasCloudSandbox()) { + await browser.connectByName( + connectionStringOrName ?? DEFAULT_CONNECTION_NAME_1 + ); + return; + } + + connectionStringOrName ??= DEFAULT_CONNECTION_STRING_1; // if the modal is still animating away when we're connecting again, things // are going to get confused @@ -52,7 +69,7 @@ export async function connectWithConnectionString( // if a connection with this name already exists, remove it otherwise we'll // add a duplicate and things will get complicated fast - const connectionName = connectionNameFromString(connectionString); + const connectionName = connectionNameFromString(connectionStringOrName); if (await browser.removeConnection(connectionName)) { debug('Removing existing connection so we do not create a duplicate', { connectionName, @@ -64,7 +81,7 @@ export async function connectWithConnectionString( await browser.setValueVisible( Selectors.ConnectionFormStringInput, - connectionString + connectionStringOrName ); await browser.doConnect(connectionName, options); @@ -173,9 +190,10 @@ export async function connectByName( } export async function connectToDefaults(browser: CompassBrowser) { - // See setupDefaultConnections() for the details behind the thinking here. - await browser.connectByName(DEFAULT_CONNECTION_NAME_1); - await browser.connectByName(DEFAULT_CONNECTION_NAME_2); + for (const name of DEFAULT_CONNECTION_NAMES) { + // See setupDefaultConnections() for the details behind the thinking here. + await browser.connectByName(name); + } // We assume that we connected successfully, so just close the success toasts // early to make sure they aren't in the way of tests. Tests that care about diff --git a/packages/compass-e2e-tests/helpers/commands/create-index.ts b/packages/compass-e2e-tests/helpers/commands/create-index.ts index a62d69cd1af..7a98414189e 100644 --- a/packages/compass-e2e-tests/helpers/commands/create-index.ts +++ b/packages/compass-e2e-tests/helpers/commands/create-index.ts @@ -9,6 +9,7 @@ type CreateIndexOptions = { wildcardProjection?: string; customCollation?: string; sparseIndex?: boolean; + rollingIndex?: boolean; }; type IndexType = '1' | '-1' | '2dsphere' | 'text'; @@ -78,7 +79,14 @@ export async function createIndex( // Select extra options if (extraOptions) { await browser.clickVisible(Selectors.IndexToggleOptions); - const { wildcardProjection } = extraOptions; + const { wildcardProjection, rollingIndex, indexName } = extraOptions; + + if (indexName) { + await browser.clickVisible(Selectors.indexToggleOption('name')); + await browser + .$(Selectors.indexOptionInput('name', 'text')) + .setValue(indexName); + } if (wildcardProjection) { await browser.clickVisible( @@ -91,6 +99,12 @@ export async function createIndex( wildcardProjection ); } + + if (rollingIndex) { + await browser.clickVisible( + Selectors.indexToggleOption('buildInRollingProcess') + ); + } } if (screenshotName) { diff --git a/packages/compass-e2e-tests/helpers/compass-web-sandbox.ts b/packages/compass-e2e-tests/helpers/compass-web-sandbox.ts new file mode 100644 index 00000000000..2f4345dc941 --- /dev/null +++ b/packages/compass-e2e-tests/helpers/compass-web-sandbox.ts @@ -0,0 +1,175 @@ +import crossSpawn from 'cross-spawn'; +import { remote } from 'webdriverio'; +import Debug from 'debug'; +import { + COMPASS_WEB_SANDBOX_RUNNER_PATH, + COMPASS_WEB_WDIO_USER_DATA_PATH, + ELECTRON_CHROMIUM_VERSION, + ELECTRON_PATH, +} from './test-runner-paths'; +import type { ConnectionInfo } from '@mongodb-js/connection-info'; +import ConnectionString from 'mongodb-connection-string-url'; + +const debug = Debug('compass-e2e-tests:compass-web-sandbox'); + +/** + * Setting up in global so that both spawned compass-web and the one started + * with webdriver will get the values + */ +process.env.OPEN_BROWSER = 'false'; // tell webpack dev server not to open the default browser +process.env.DISABLE_DEVSERVER_OVERLAY = 'false'; +process.env.APP_ENV = 'webdriverio'; + +const wait = (ms: number) => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +}; + +export function spawnCompassWebSandbox() { + const proc = crossSpawn.spawn( + 'npm', + ['run', '--unsafe-perm', 'start', '--workspace', '@mongodb-js/compass-web'], + { env: process.env } + ); + proc.stdout.pipe(process.stdout); + proc.stderr.pipe(process.stderr); + return proc; +} + +export async function waitForCompassWebSandboxToBeReady( + sandboxUrl: string, + signal: AbortSignal +) { + let serverReady = false; + const start = Date.now(); + while (!serverReady) { + if (signal.aborted) { + return; + } + if (Date.now() - start >= 120_000) { + throw new Error( + 'The compass-web sandbox is still not running after 120000ms' + ); + } + // No point in trying to fetch sandbox URL right away, give the spawn script + // some time to run + await wait(2000); + try { + const res = await fetch(sandboxUrl); + serverReady = res.ok; + debug('Web server ready:', serverReady); + } catch (err) { + debug('Failed to connect to dev server:', (err as any).message); + } + } +} + +export async function spawnCompassWebSandboxAndSignInToAtlas( + { + username, + password, + sandboxUrl, + waitforTimeout, + }: { + username: string; + password: string; + sandboxUrl: string; + waitforTimeout: number; + }, + signal: AbortSignal +) { + debug('Starting electron-proxy using webdriver ...'); + + const electronProxyRemote = await remote({ + capabilities: { + browserName: 'chromium', + browserVersion: ELECTRON_CHROMIUM_VERSION, + 'goog:chromeOptions': { + binary: ELECTRON_PATH, + args: [ + `--user-data-dir=${COMPASS_WEB_WDIO_USER_DATA_PATH}`, + `--app=${COMPASS_WEB_SANDBOX_RUNNER_PATH}`, + ], + }, + }, + waitforTimeout, + }); + + if (signal.aborted) { + return electronProxyRemote; + } + + debug('Signing in to Atlas as %s ...', username); + + const authenticatePromise = fetch(`${sandboxUrl}/authenticate`, { + method: 'POST', + }); + + const authWindowHandler = await electronProxyRemote.waitUntil(async () => { + const handlers = await electronProxyRemote.getWindowHandles(); + // First window is about:blank, second one is the one we triggered above + // with `/authenticate` request + return handlers[1]; + }); + await electronProxyRemote.switchToWindow(authWindowHandler); + + await electronProxyRemote.$('input[name="username"]').waitForEnabled(); + await electronProxyRemote.$('input[name="username"]').setValue(username); + + await electronProxyRemote.$('button=Next').waitForEnabled(); + await electronProxyRemote.$('button=Next').click(); + + await electronProxyRemote.$('input[name="password"]').waitForEnabled(); + await electronProxyRemote.$('input[name="password"]').setValue(password); + + await electronProxyRemote.$('button=Login').waitForEnabled(); + await electronProxyRemote.$('button=Login').click(); + + if (signal.aborted) { + return electronProxyRemote; + } + + debug('Waiting for the auth to finish ...'); + + const res = await authenticatePromise; + + if (res.ok === false || !(await res.json()).projectId) { + throw new Error( + `Failed to authenticate in Atlas Cloud: ${res.statusText} (${res.status})` + ); + } + + if (signal.aborted) { + return electronProxyRemote; + } + + debug('Waiting for x509 cert to propagate to Atlas clusters ...'); + + await fetch(`${sandboxUrl}/x509`); + + return electronProxyRemote; +} + +export const getAtlasCloudSandboxDefaultConnections = ( + connectionsString: string, + dbUser: string, + dbPassword: string +) => { + type AtlasCloudSandboxDefaultConnections = Record< + string, + { standard: string; standardSrv: string } + >; + const connections: AtlasCloudSandboxDefaultConnections = + JSON.parse(connectionsString); + return Object.entries(connections).map(([name, cluster]): ConnectionInfo => { + const str = new ConnectionString(cluster.standardSrv ?? cluster.standard); + str.username = dbUser; + str.password = dbPassword; + return { + id: name, + connectionOptions: { connectionString: String(str) }, + favorite: { name }, + }; + }); +}; diff --git a/packages/compass-e2e-tests/helpers/compass.ts b/packages/compass-e2e-tests/helpers/compass.ts index 6f518bdd516..02e7ca37bc5 100644 --- a/packages/compass-e2e-tests/helpers/compass.ts +++ b/packages/compass-e2e-tests/helpers/compass.ts @@ -23,7 +23,6 @@ import type { CompassBrowser } from './compass-browser'; import type { LogEntry } from './telemetry'; import Debug from 'debug'; import semver from 'semver'; -import crossSpawn from 'cross-spawn'; import { CHROME_STARTUP_FLAGS } from './chrome-startup-flags'; import { DEFAULT_CONNECTION_STRINGS, @@ -44,6 +43,19 @@ import { LOG_SCREENSHOTS_PATH, ELECTRON_PATH, } from './test-runner-paths'; +import treeKill from 'tree-kill'; + +const killAsync = async (pid: number, signal?: string) => { + return new Promise((resolve, reject) => { + treeKill(pid, signal ?? 'SIGTERM', (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +}; const debug = Debug('compass-e2e-tests'); @@ -666,7 +678,7 @@ async function startCompassElectron( return ( p.ppid === process.pid && (p.cmd?.startsWith(binary) || - /(MongoDB Compass|Electron|electron)/.test(p.name)) + /(MongoDB Compass|Electron|electron|chromedriver)/.test(p.name)) ); }); @@ -688,7 +700,7 @@ async function startCompassElectron( ); for (const p of filteredProcesses) { - tryKillProcess(p.pid, p.name); + await killAsync(p.pid); } throw err; } @@ -803,21 +815,6 @@ export async function startBrowser( return compass; } -function tryKillProcess(pid: number, name = ''): void { - try { - debug(`Killing process ${name} with PID ${pid}`); - if (process.platform === 'win32') { - crossSpawn.sync('taskkill', ['/PID', String(pid), '/F', '/T']); - } else { - process.kill(pid); - } - } catch (err) { - debug(`Failed to kill process ${name} with PID ${pid}`, { - error: (err as Error).stack, - }); - } -} - /** * @param {string} logPath The compass application log path * @returns {Promise} @@ -1096,7 +1093,7 @@ export async function cleanup(compass?: Compass): Promise { // this only works for the electron use case try { debug(`Trying to manually kill Compass [${compass.name}]`); - tryKillProcess(compass.mainProcessPid, compass.name); + await killAsync(compass.mainProcessPid); } catch { /* already logged ... */ } diff --git a/packages/compass-e2e-tests/helpers/insert-data.ts b/packages/compass-e2e-tests/helpers/insert-data.ts index 144e9b746dd..ab0fa1894b8 100644 --- a/packages/compass-e2e-tests/helpers/insert-data.ts +++ b/packages/compass-e2e-tests/helpers/insert-data.ts @@ -1,6 +1,7 @@ import { MongoClient } from 'mongodb'; import type { Db, MongoServerError } from 'mongodb'; import { DEFAULT_CONNECTION_STRINGS } from './test-runner-context'; +import { redactConnectionString } from 'mongodb-connection-string-url'; // This is a list of all the known database names that get created by tests so // that we can know what to drop when we clean up before every test. If a new @@ -55,10 +56,14 @@ export const beforeAll = async () => { await Promise.all(clients.map((client) => client.connect())); + const connectionsForPrinting = connectionStrings + .map((str) => { + return redactConnectionString(str); + }) + .join(' and '); + console.log( - `Connected successfully to ${connectionStrings.join( - ' and ' - )} for inserting data` + `Connected successfully to ${connectionsForPrinting} for inserting data` ); test_dbs = clients.map((client) => client.db('test')); diff --git a/packages/compass-e2e-tests/helpers/test-runner-context.ts b/packages/compass-e2e-tests/helpers/test-runner-context.ts index f76a729dd31..4ad3fe13892 100644 --- a/packages/compass-e2e-tests/helpers/test-runner-context.ts +++ b/packages/compass-e2e-tests/helpers/test-runner-context.ts @@ -8,6 +8,7 @@ import type { Argv } from 'yargs'; import { hideBin } from 'yargs/helpers'; import Debug from 'debug'; import fs from 'fs'; +import { getAtlasCloudSandboxDefaultConnections } from './compass-web-sandbox'; const debug = Debug('compass-e2e-tests:context'); @@ -112,6 +113,16 @@ const atlasCloudExternalArgs = [ 'atlas-cloud-external-default-connections-file', ] as const; +const atlasCloudSandboxArgs = [ + 'test-atlas-cloud-sandbox', + 'atlas-cloud-sandbox-cloud-config', + 'atlas-cloud-sandbox-username', + 'atlas-cloud-sandbox-password', + 'atlas-cloud-sandbox-dbuser-username', + 'atlas-cloud-sandbox-dbuser-password', + 'atlas-cloud-sandbox-default-connections', +] as const; + let testEnv: 'desktop' | 'web' | undefined; function buildWebArgs(yargs: Argv) { @@ -146,6 +157,47 @@ function buildWebArgs(yargs: Argv) { description: 'Set compass-web sandbox URL', default: 'http://localhost:7777', }) + .option('test-atlas-cloud-sandbox', { + type: 'boolean', + description: + 'Run compass-web tests against a sandbox with a singed in Atlas Cloud user (allows to test Atlas-only functionality that is only available for Cloud UI backend)', + }) + .options('atlas-cloud-sandbox-cloud-config', { + choices: ['local', 'dev', 'qa', 'prod'] as const, + description: 'Atlas Cloud config preset for the sandbox', + }) + .options('atlas-cloud-sandbox-username', { + type: 'string', + description: + 'Atlas Cloud username. Will be used to sign in to an account before running the tests', + }) + .options('atlas-cloud-sandbox-password', { + type: 'string', + description: + 'Atlas Cloud user password. Will be used to sign in to an account before running the tests', + }) + .options('atlas-cloud-sandbox-dbuser-username', { + type: 'string', + description: + 'Atlas Cloud database username. Will be used to prepolulate cluster with data', + }) + .options('atlas-cloud-sandbox-dbuser-password', { + type: 'string', + description: + 'Atlas Cloud user database user password. Will be used to prepolulate cluster with data', + }) + .options('atlas-cloud-sandbox-default-connections', { + type: 'string', + description: + 'Stringified JSON with connections that are expected to be available in the Atlas project', + }) + .implies( + Object.fromEntries( + atlasCloudSandboxArgs.map((arg) => { + return [arg, atlasCloudSandboxArgs]; + }) + ) + ) .option('test-atlas-cloud-external', { type: 'boolean', description: @@ -176,6 +228,10 @@ function buildWebArgs(yargs: Argv) { }) ) ) + .conflicts({ + 'test-atlas-cloud-external': 'test-atlas-cloud-sandbox', + 'test-atlas-cloud-sandbox': 'test-atlas-cloud-external', + }) .epilogue( 'All command line arguments can be also provided as env vars with `COMPASS_E2E_` prefix:\n\n COMPASS_E2E_TEST_ATLAS_CLOUD_EXTERNAL=true compass-e2e-tests web' ) @@ -266,10 +322,49 @@ export function isTestingAtlasCloudExternal( return isTestingWeb(ctx) && !!ctx.testAtlasCloudExternal; } -debug('Running tests with the following arguments:', context); +export function isTestingAtlasCloudSandbox( + ctx = context +): ctx is WebParsedArgs & { + [K in + | 'testAtlasCloudSandbox' + | 'atlasCloudSandboxUsername' + | 'atlasCloudSandboxPassword' + | 'atlasCloudSandboxDbuserUsername' + | 'atlasCloudSandboxDbuserPassword' + | 'atlasCloudSandboxDefaultConnections']: NonNullable; +} { + return isTestingWeb(ctx) && !!ctx.testAtlasCloudSandbox; +} + +export function assertTestingAtlasCloudSandbox( + ctx = context +): asserts ctx is WebParsedArgs & { + [K in + | 'testAtlasCloudSandbox' + | 'atlasCloudSandboxUsername' + | 'atlasCloudSandboxPassword' + | 'atlasCloudSandboxDbuserUsername' + | 'atlasCloudSandboxDbuserPassword' + | 'atlasCloudSandboxDefaultConnections']: NonNullable; +} { + if (!isTestingAtlasCloudSandbox(ctx)) { + throw new Error(`Expected tested runtime to be web w/ Atlas Cloud account`); + } +} + +const contextForPrinting = Object.fromEntries( + Object.entries(context).map(([k, v]) => { + return [k, /password/i.test(k) ? '' : v]; + }) +); + +debug('Running tests with the following arguments:', contextForPrinting); process.env.HADRON_DISTRIBUTION ??= context.hadronDistribution; +process.env.COMPASS_WEB_HTTP_PROXY_CLOUD_CONFIG ??= + context.atlasCloudSandboxCloudConfig ?? 'dev'; + const testServerVersion = process.env.MONGODB_VERSION ?? process.env.MONGODB_RUNNER_VERSION; @@ -279,6 +374,12 @@ export const DEFAULT_CONNECTIONS: (ConnectionInfo & { ? JSON.parse( fs.readFileSync(context.atlasCloudExternalDefaultConnectionsFile, 'utf-8') ) + : isTestingAtlasCloudSandbox(context) + ? getAtlasCloudSandboxDefaultConnections( + context.atlasCloudSandboxDefaultConnections, + context.atlasCloudSandboxDbuserUsername, + context.atlasCloudSandboxDbuserPassword + ) : [ { id: 'test-connection-1', diff --git a/packages/compass-e2e-tests/helpers/test-runner-global-fixtures.ts b/packages/compass-e2e-tests/helpers/test-runner-global-fixtures.ts index 538bf555fb0..9c163d61b60 100644 --- a/packages/compass-e2e-tests/helpers/test-runner-global-fixtures.ts +++ b/packages/compass-e2e-tests/helpers/test-runner-global-fixtures.ts @@ -5,13 +5,13 @@ import { DEFAULT_CONNECTIONS, DEFAULT_CONNECTIONS_SERVER_INFO, isTestingAtlasCloudExternal, + isTestingAtlasCloudSandbox, isTestingDesktop, isTestingWeb, } from './test-runner-context'; import { E2E_WORKSPACE_PATH, LOG_PATH } from './test-runner-paths'; import Debug from 'debug'; import { startTestServer } from '@mongodb-js/compass-test-server'; -import crossSpawn from 'cross-spawn'; import kill from 'tree-kill'; import { MongoClient } from 'mongodb'; import { isEnterprise } from 'mongodb-build-info'; @@ -22,6 +22,11 @@ import { removeUserDataDir, } from './compass'; import { getConnectionTitle } from '@mongodb-js/connection-info'; +import { + spawnCompassWebSandbox, + spawnCompassWebSandboxAndSignInToAtlas, + waitForCompassWebSandboxToBeReady, +} from './compass-web-sandbox'; export const globalFixturesAbortController = new AbortController(); @@ -35,11 +40,6 @@ export let abortRunner: (() => void) | undefined; const debug = Debug('compass-e2e-tests:mocha-global-fixtures'); -const wait = (ms: number) => - new Promise((resolve) => { - setTimeout(resolve, ms); - }); - const cleanupFns: (() => Promise | void)[] = []; /** @@ -92,16 +92,34 @@ export async function mochaGlobalSetup(this: Mocha.Runner) { if (isTestingWeb(context) && !isTestingAtlasCloudExternal(context)) { debug('Starting Compass Web server ...'); - const compassWeb = spawnCompassWeb(); - cleanupFns.push(() => { - if (compassWeb.pid) { - debug(`Killing compass-web [${compassWeb.pid}]`); - kill(compassWeb.pid, 'SIGINT'); - } else { - debug('No pid for compass-web'); - } - }); - await waitForCompassWebToBeReady(context.sandboxUrl); + if (isTestingAtlasCloudSandbox(context)) { + const compassWeb = await spawnCompassWebSandboxAndSignInToAtlas( + { + username: context.atlasCloudSandboxUsername, + password: context.atlasCloudSandboxPassword, + sandboxUrl: context.sandboxUrl, + waitforTimeout: context.webdriverWaitforTimeout, + }, + globalFixturesAbortController.signal + ); + cleanupFns.push(async () => { + await compassWeb.deleteSession({ shutdownDriver: true }); + }); + } else { + const compassWeb = spawnCompassWebSandbox(); + cleanupFns.push(() => { + if (compassWeb.pid) { + debug(`Killing compass-web [${compassWeb.pid}]`); + kill(compassWeb.pid, 'SIGINT'); + } else { + debug('No pid for compass-web'); + } + }); + await waitForCompassWebSandboxToBeReady( + context.sandboxUrl, + globalFixturesAbortController.signal + ); + } } } @@ -160,45 +178,6 @@ export async function mochaGlobalTeardown() { ); } -function spawnCompassWeb() { - const proc = crossSpawn.spawn( - 'npm', - ['run', '--unsafe-perm', 'start', '--workspace', '@mongodb-js/compass-web'], - { - env: { - ...process.env, - OPEN_BROWSER: 'false', // tell webpack dev server not to open the default browser - DISABLE_DEVSERVER_OVERLAY: 'true', - APP_ENV: 'webdriverio', - }, - } - ); - proc.stdout.pipe(process.stdout); - proc.stderr.pipe(process.stderr); - return proc; -} - -async function waitForCompassWebToBeReady(sandboxUrl: string) { - let serverReady = false; - const start = Date.now(); - while (!serverReady) { - throwIfAborted(); - if (Date.now() - start >= 120_000) { - throw new Error( - 'The compass-web sandbox is still not running after 120000ms' - ); - } - try { - const res = await fetch(sandboxUrl); - serverReady = res.ok; - debug('Web server ready:', serverReady); - } catch (err) { - debug('Failed to connect to dev server:', (err as any).message); - } - await wait(1000); - } -} - async function updateMongoDBServerInfo() { try { for (const { connectionOptions } of DEFAULT_CONNECTIONS) { diff --git a/packages/compass-e2e-tests/helpers/test-runner-paths.ts b/packages/compass-e2e-tests/helpers/test-runner-paths.ts index 19b835abe7b..eae834363b4 100644 --- a/packages/compass-e2e-tests/helpers/test-runner-paths.ts +++ b/packages/compass-e2e-tests/helpers/test-runner-paths.ts @@ -3,6 +3,7 @@ import electronPath from 'electron'; import electronPackageJson from 'electron/package.json'; // @ts-expect-error no types for this package import { electronToChromium } from 'electron-to-chromium'; +import os from 'os'; if (typeof electronPath !== 'string') { throw new Error( @@ -34,3 +35,13 @@ export const COVERAGE_PATH = (process.env.COVERAGE = MONOREPO_ROOT_PATH); export const ELECTRON_PATH = electronPath; export const ELECTRON_VERSION = electronPackageJson.version; export const ELECTRON_CHROMIUM_VERSION = electronToChromium(ELECTRON_VERSION); + +export const COMPASS_WEB_SANDBOX_RUNNER_PATH = path.resolve( + path.dirname(require.resolve('@mongodb-js/compass-web/package.json')), + 'scripts', + 'electron-proxy.js' +); +export const COMPASS_WEB_WDIO_USER_DATA_PATH = path.resolve( + os.tmpdir(), + `wdio-electron-proxy-${Date.now()}` +); diff --git a/packages/compass-e2e-tests/tests/atlas-cloud/rolling-indexes.test.ts b/packages/compass-e2e-tests/tests/atlas-cloud/rolling-indexes.test.ts new file mode 100644 index 00000000000..4c2fa9aac5e --- /dev/null +++ b/packages/compass-e2e-tests/tests/atlas-cloud/rolling-indexes.test.ts @@ -0,0 +1,77 @@ +import type { Compass } from '../../helpers/compass'; +import { cleanup, init, Selectors } from '../../helpers/compass'; +import type { CompassBrowser } from '../../helpers/compass-browser'; +import { createNumbersCollection } from '../../helpers/insert-data'; +import { + DEFAULT_CONNECTION_NAMES, + isTestingAtlasCloudSandbox, +} from '../../helpers/test-runner-context'; + +describe('Rolling indexes', function () { + let compass: Compass; + let browser: CompassBrowser; + + before(async function () { + compass = await init(this.test?.fullTitle()); + browser = compass.browser; + await browser.setupDefaultConnections(); + }); + + before(function () { + if (!isTestingAtlasCloudSandbox()) { + this.skip(); + } + }); + + after(async function () { + await cleanup(compass); + }); + + it('should be able to create, list, and delete rolling indexes', async function () { + // Building rolling indexes is a slow process + const extendedRollingIndexesTimeout = 1000 * 60 * 20; + + this.timeout(extendedRollingIndexesTimeout * 1.2); + + await createNumbersCollection(); + await browser.connectToDefaults(); + await browser.navigateToCollectionTab( + DEFAULT_CONNECTION_NAMES[0], + 'test', + 'numbers', + 'Indexes' + ); + + const indexName = 'compass-e2e-rolling-build-index-for-testing'; + + // Fail fast if index with this name already exists: should never happen as + // every run in CI gets their own cluster but if we try to create over + // existing or one being created, it can bork the whole cluster and we don't + // want that + await browser + .$(Selectors.indexComponent(indexName)) + .waitForDisplayed({ reverse: true }); + + await browser.createIndex( + { fieldName: 'i', indexType: '1' }, + { rollingIndex: true, indexName } + ); + + // Special rolling index badge indicating that build has started (we got it + // listed by automation agent) + await browser + .$(Selectors.indexComponent(indexName)) + .$('[data-testid="index-building"]') + .waitForDisplayed(); + + // Now wait for index to finish building + await browser + .$(Selectors.indexComponent(indexName)) + .$('[data-testid="index-ready"]') + .waitForDisplayed({ timeout: extendedRollingIndexesTimeout }); + + // Now that it's ready, delete it (it will also check that it's eventually + // removed from the list) + await browser.dropIndex(indexName); + }); +}); diff --git a/packages/compass-indexes/src/components/create-index-form/checkbox-input.tsx b/packages/compass-indexes/src/components/create-index-form/checkbox-input.tsx index 9e7916625f0..3590ad90475 100644 --- a/packages/compass-indexes/src/components/create-index-form/checkbox-input.tsx +++ b/packages/compass-indexes/src/components/create-index-form/checkbox-input.tsx @@ -26,17 +26,21 @@ export const CheckboxInput: React.FunctionComponent = ({ checked, onChange, }) => { - const labelId = `create-index-modal-${name}-checkbox`; + const labelId = `create-index-modal-${name}`; return ( { onChange(name, event.target.checked); }} - label={} + label={ + + } // @ts-expect-error leafygreen types only allow strings here, but can // render a ReactNode too (and we use that to render links inside // descriptions) diff --git a/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx b/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx index e87d8370f3d..00b04fb64fb 100644 --- a/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx +++ b/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx @@ -55,6 +55,13 @@ function ToastBodyWithAction({ ); } +const IS_CI = + process.env.ci || + process.env.CI || + process.env.IS_CI || + process.env.NODE_ENV === 'test' || + process.env.APP_ENV === 'webdriverio'; + export function useAtlasProxySignIn(): AtlasLoginReturnValue { const [status, setStatus] = useState('checking'); const [projectId, setProjectId] = useState(null); @@ -99,6 +106,9 @@ export function useAtlasProxySignIn(): AtlasLoginReturnValue { } setProjectId(projectId); setStatus('signed-in'); + if (IS_CI) { + return; + } openToast('atlas-proxy', { title: 'Signed in to local Atlas Cloud proxy', description: ( @@ -116,13 +126,7 @@ export function useAtlasProxySignIn(): AtlasLoginReturnValue { .catch(() => { if (mounted) { setStatus('signed-out'); - if ( - process.env.ci || - process.env.CI || - process.env.IS_CI || - process.env.NODE_ENV === 'test' || - process.env.APP_ENV === 'webdriverio' - ) { + if (IS_CI) { return; } openToast('atlas-proxy', { diff --git a/packages/compass-web/scripts/electron-proxy.js b/packages/compass-web/scripts/electron-proxy.js index 2d7db2d6805..df51a2d87bb 100644 --- a/packages/compass-web/scripts/electron-proxy.js +++ b/packages/compass-web/scripts/electron-proxy.js @@ -542,6 +542,11 @@ function cleanupAndExit() { } electronApp.whenReady().then(async () => { + // Create an empty browser window so that webdriver session can be + // immediately get attached to something without failing + const emptyBrowserWindow = new BrowserWindow({ show: false }); + emptyBrowserWindow.loadURL('about:blank'); + electronApp.on('window-all-closed', () => { // We want proxy to keep running even when all the windows are closed, but // hide the dock icon because there are not windows associated with it