From 0d53e6c69a565130096a896f7e02cd404dc5fef7 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 26 Aug 2024 20:04:08 +0200 Subject: [PATCH] fixup: add e2e tests --- packages/compass-e2e-tests/.depcheckrc | 3 +- .../helpers/commands/connect-form.ts | 6 ++ .../helpers/connect-form-state.ts | 3 +- packages/compass-e2e-tests/helpers/proxy.ts | 71 ++++++++++++++++ .../compass-e2e-tests/helpers/selectors.ts | 10 +++ packages/compass-e2e-tests/tests/oidc.test.ts | 81 +++++++++++++++++++ .../compass-e2e-tests/tests/proxy.test.ts | 56 ++++++++++++- .../authentication-oidc.tsx | 2 +- .../data-service/src/connect-mongo-client.ts | 4 + 9 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 packages/compass-e2e-tests/helpers/proxy.ts diff --git a/packages/compass-e2e-tests/.depcheckrc b/packages/compass-e2e-tests/.depcheckrc index 565004c4a61..cb62adfaac8 100644 --- a/packages/compass-e2e-tests/.depcheckrc +++ b/packages/compass-e2e-tests/.depcheckrc @@ -4,4 +4,5 @@ ignores: - '@wdio/types' - 'mongodb-compass' - 'ps-list' - - 'mongodb-runner' \ No newline at end of file + - 'mongodb-runner' + - '@electron/remote' diff --git a/packages/compass-e2e-tests/helpers/commands/connect-form.ts b/packages/compass-e2e-tests/helpers/commands/connect-form.ts index ce497ff5af1..60cd0784f4a 100644 --- a/packages/compass-e2e-tests/helpers/commands/connect-form.ts +++ b/packages/compass-e2e-tests/helpers/commands/connect-form.ts @@ -644,6 +644,12 @@ export async function setConnectFormState( state.oidcUsername ); } + if (state.oidcUseApplicationProxy === false) { + await browser.expandAccordion(Selectors.ConnectionFormOIDCAdvancedToggle); + await browser.clickParent( + Selectors.ConnectionFormOIDCUseApplicationProxyCheckbox + ); + } } // FLE2 diff --git a/packages/compass-e2e-tests/helpers/connect-form-state.ts b/packages/compass-e2e-tests/helpers/connect-form-state.ts index bb7ac9eff32..9f1c087da39 100644 --- a/packages/compass-e2e-tests/helpers/connect-form-state.ts +++ b/packages/compass-e2e-tests/helpers/connect-form-state.ts @@ -41,6 +41,7 @@ export interface ConnectFormState { // - OIDC oidcUsername?: string; // (Principal). + oidcUseApplicationProxy?: boolean; // - AWS IAM awsAccessKeyId?: string; @@ -57,7 +58,7 @@ export interface ConnectFormState { tlsAllowInvalidCertificates?: boolean; // Proxy/SSH - proxyMethod?: 'none' | 'password' | 'identity' | 'socks'; + proxyMethod?: 'none' | 'password' | 'identity' | 'socks' | 'app-proxy'; // FLE2 fleKeyVaultNamespace?: string; diff --git a/packages/compass-e2e-tests/helpers/proxy.ts b/packages/compass-e2e-tests/helpers/proxy.ts new file mode 100644 index 00000000000..ab86ec8e574 --- /dev/null +++ b/packages/compass-e2e-tests/helpers/proxy.ts @@ -0,0 +1,71 @@ +import type { + Server as HTTPServer, + IncomingMessage, + ServerResponse, +} from 'http'; +import { request } from 'http'; +import type { Socket } from 'net'; +import { connect } from 'net'; + +export interface ProxyHandlersResult { + connectRequests: IncomingMessage[]; + httpForwardRequests: IncomingMessage[]; + connections: Socket[]; +} + +export function setupProxyServer(server: HTTPServer): ProxyHandlersResult { + const connectRequests: IncomingMessage[] = []; + const httpForwardRequests: IncomingMessage[] = []; + const connections: Socket[] = []; + + server.on('connect', onconnect); + server.on('request', onrequest); + function onconnect( + this: HTTPServer, + req: IncomingMessage, + socket: Socket, + head: Buffer + ): void { + (req as any).server = this; + let host: string; + let port: string; + if (req.url?.includes(']:')) { + [host, port] = req.url.slice(1).split(']:'); + } else { + [host, port] = (req.url ?? '').split(':'); + } + if (host === 'compass.mongodb.com' || host === 'downloads.mongodb.com') { + // The snippet loader and update notifier can reach out to thes endpoints, + // but we usually do not actually wait for this to happen or not in CI, + // so we're just ignoring these requests here to avoid flaky behavior. + socket.end(); + return; + } + connectRequests.push(req); + socket.unshift(head); + socket.write('HTTP/1.0 200 OK\r\n\r\n'); + const outbound = connect(+port, host); + socket.pipe(outbound).pipe(socket); + // socket.on('data', chk => console.log('[from client] ' + chk.toString())); + // outbound.on('data', chk => console.log('[from server] ' + chk.toString())); + const cleanup = () => { + outbound.destroy(); + socket.destroy(); + }; + outbound.on('error', cleanup); + socket.on('error', cleanup); + connections.push(socket, outbound); + } + function onrequest(req: IncomingMessage, res: ServerResponse) { + httpForwardRequests.push(req); + const proxyReq = request( + req.url!, + { method: req.method, headers: req.headers }, + (proxyRes) => proxyRes.pipe(res) + ); + if (req.method === 'GET') proxyReq.end(); + else req.pipe(proxyReq); + } + + return { connections, connectRequests, httpForwardRequests }; +} diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index e920193e6fe..1fde25106af 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -103,6 +103,10 @@ export const ConnectionFormInputPlainPassword = '[data-testid="connection-plain-password-input"]'; export const ConnectionFormInputOIDCUsername = '[data-testid="connection-oidc-username-input"]'; +export const ConnectionFormOIDCAdvancedToggle = + '[data-testid="oidc-advanced-options"]'; +export const ConnectionFormOIDCUseApplicationProxyCheckbox = + '[data-testid="oidc-use-application-level-proxy"]'; export const ConnectionFormInputAWSAccessKeyId = '[data-testid="connection-form-aws-access-key-id-input"]'; export const ConnectionFormInputAWSSecretAccessKey = @@ -1387,5 +1391,11 @@ export const AtlasLoginStatus = '[data-testid="atlas-login-status"]'; export const AtlasLoginErrorToast = '#atlas-sign-in-error'; export const AgreeAndContinueButton = 'button=Agree and continue'; +// Proxy settings +export const ProxyUrl = + '[data-testid="proxy-settings"] [data-testid="proxy-url"]'; +export const ProxyCustomButton = + '[data-testid="proxy-settings"] [data-testid="custom-radio"]'; + // Close tab confirmation export const ConfirmTabCloseModal = '[data-testid="confirm-tab-close"]'; diff --git a/packages/compass-e2e-tests/tests/oidc.test.ts b/packages/compass-e2e-tests/tests/oidc.test.ts index d1db1b0ab0e..cbcf2758286 100644 --- a/packages/compass-e2e-tests/tests/oidc.test.ts +++ b/packages/compass-e2e-tests/tests/oidc.test.ts @@ -10,6 +10,7 @@ import { connectionNameFromString, TEST_MULTIPLE_CONNECTIONS, } from '../helpers/compass'; +import { setupProxyServer } from '../helpers/proxy'; import * as Selectors from '../helpers/selectors'; import type { Compass } from '../helpers/compass'; import type { OIDCMockProviderConfig } from '@mongodb-js/oidc-mock-provider'; @@ -18,6 +19,9 @@ import path from 'path'; import os from 'os'; import { promises as fs } from 'fs'; import { once, EventEmitter } from 'events'; +import type { Server as HTTPServer, IncomingMessage } from 'http'; +import { createServer as createHTTPServer } from 'http'; +import type { Socket, AddressInfo } from 'net'; import { expect } from 'chai'; import type { MongoCluster } from '@mongodb-js/compass-test-server'; import { startTestServer } from '@mongodb-js/compass-test-server'; @@ -487,4 +491,81 @@ describe('OIDC integration', function () { expect(oidcMockProviderEndpointAccesses['/authorize']).to.equal(1); }); + + context('when using a proxy', function () { + let httpServer: HTTPServer; + let connectRequests: IncomingMessage[]; + let httpForwardRequests: IncomingMessage[]; + let connections: Socket[]; + + beforeEach(async function () { + await browser.execute(function () { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { safeStorage } = require('@electron/remote'); + if (!safeStorage.isEncryptionAvailable()) + safeStorage.setUsePlainTextEncryption(true); + if (!safeStorage.isEncryptionAvailable()) + throw new Error('encryption not available on this platform'); + }); + await browser.setFeature('proxy', ''); + await browser.setFeature('enableProxySupport', true); + httpServer = createHTTPServer(); + ({ connectRequests, httpForwardRequests, connections } = + setupProxyServer(httpServer)); + httpServer.listen(0); + await once(httpServer, 'listening'); + }); + + afterEach(async function () { + await browser.setFeature('proxy', ''); + httpServer?.close?.(); + for (const conn of connections) { + if (!conn.destroyed) conn.destroy(); + } + }); + + it('can proxy both HTTP and MongoDB traffic through a proxy', async function () { + await browser.openSettingsModal('proxy'); + await browser.clickParent(Selectors.ProxyCustomButton); + await browser.setValueVisible( + Selectors.ProxyUrl, + `http://localhost:${(httpServer.address() as AddressInfo).port}` + ); + await browser.clickVisible(Selectors.SaveSettingsButton); + + await browser.connectWithConnectionForm({ + hosts: [hostport], + authMethod: 'MONGODB-OIDC', + connectionName, + oidcUseApplicationProxy: true, + proxyMethod: 'app-proxy', + }); + + expect(connectRequests.map((c) => c.url)).to.include(hostport); + expect(httpForwardRequests.map((c) => c.url)).to.include( + `${oidcMockProvider.issuer}/.well-known/openid-configuration` + ); + }); + + it('can choose not to forward OIDC HTTP traffic', async function () { + await browser.openSettingsModal('proxy'); + await browser.clickParent(Selectors.ProxyCustomButton); + await browser.setValueVisible( + Selectors.ProxyUrl, + `http://localhost:${(httpServer.address() as AddressInfo).port}` + ); + await browser.clickVisible(Selectors.SaveSettingsButton); + + await browser.connectWithConnectionForm({ + hosts: [hostport], + authMethod: 'MONGODB-OIDC', + connectionName, + oidcUseApplicationProxy: false, + proxyMethod: 'app-proxy', + }); + + expect(connectRequests.map((c) => c.url)).to.include(hostport); + expect(httpForwardRequests.map((c) => c.url)).to.be.empty; + }); + }); }); diff --git a/packages/compass-e2e-tests/tests/proxy.test.ts b/packages/compass-e2e-tests/tests/proxy.test.ts index 7a56c6807ef..e5c20cbaf65 100644 --- a/packages/compass-e2e-tests/tests/proxy.test.ts +++ b/packages/compass-e2e-tests/tests/proxy.test.ts @@ -7,10 +7,12 @@ import { import { expect } from 'chai'; import type { Compass } from '../helpers/compass'; import type { CompassBrowser } from '../helpers/compass-browser'; -import type { Server as HTTPServer } from 'http'; +import type { Server as HTTPServer, IncomingMessage } from 'http'; import { createServer as createHTTPServer } from 'http'; -import type { AddressInfo, Server } from 'net'; +import type { AddressInfo, Server, Socket } from 'net'; import { once } from 'events'; +import { setupProxyServer } from '../helpers/proxy'; +import * as Selectors from '../helpers/selectors'; async function listen(srv: Server): Promise { srv.listen(0); @@ -83,4 +85,54 @@ describe('Proxy support', function () { }); expect(result).to.equal('hello, http://compass.mongodb.com/ (proxy2)'); }); + + context('when connecting to a cluster', function () { + let connectRequests: IncomingMessage[]; + let connections: Socket[]; + + beforeEach(async function () { + compass = await init(this.test?.fullTitle()); + browser = compass.browser; + + await browser.execute(function () { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { safeStorage } = require('@electron/remote'); + if (!safeStorage.isEncryptionAvailable()) + safeStorage.setUsePlainTextEncryption(true); + if (!safeStorage.isEncryptionAvailable()) + throw new Error('encryption not available on this platform'); + }); + await browser.setFeature('proxy', ''); + await browser.setFeature('enableProxySupport', true); + httpProxyServer1.removeAllListeners('request'); + ({ connectRequests, connections } = setupProxyServer(httpProxyServer1)); + }); + + afterEach(async function () { + await browser?.setFeature('proxy', ''); + for (const conn of connections) { + if (!conn.destroyed) conn.destroy(); + } + }); + + it('can proxy MongoDB traffic through a proxy', async function () { + await browser.openSettingsModal('proxy'); + await browser.clickParent(Selectors.ProxyCustomButton); + await browser.setValueVisible( + Selectors.ProxyUrl, + `http://localhost:${(httpProxyServer1.address() as AddressInfo).port}` + ); + await browser.clickVisible(Selectors.SaveSettingsButton); + + const hostport = '127.0.0.1:27091'; + const connectionName = this.test?.fullTitle() ?? ''; + await browser.connectWithConnectionForm({ + hosts: [hostport], + connectionName, + proxyMethod: 'app-proxy', + }); + + expect(connectRequests.map((c) => c.url)).to.include(hostport); + }); + }); }); diff --git a/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-oidc.tsx b/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-oidc.tsx index aaed9eaedf1..a93ea99eac9 100644 --- a/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-oidc.tsx +++ b/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-oidc.tsx @@ -82,7 +82,7 @@ function AuthenticationOIDC({ /> - +