From 654a80c81bf801d4f32e412339b1a23b0b0b0065 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 20 Mar 2025 13:45:14 +0100 Subject: [PATCH 01/11] feat: handle deep links to commands --- package.json | 3 ++- src/commands/index.ts | 1 + src/connectionController.ts | 27 +++++++++++++++++++-------- src/mdbExtensionController.ts | 30 ++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 91e69fe81..2079dfcc5 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,8 @@ "onChatParticipant:mongodb.participant", "onLanguage:json", "onLanguage:javascript", - "onLanguage:plaintext" + "onLanguage:plaintext", + "onUri" ], "contributes": { "chatParticipants": [ diff --git a/src/commands/index.ts b/src/commands/index.ts index 348189649..935ebda25 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,6 +1,7 @@ enum EXTENSION_COMMANDS { MDB_CONNECT = 'mdb.connect', MDB_CONNECT_WITH_URI = 'mdb.connectWithURI', + MDB_CONNECT_WITH_CONNECTION_STRING = 'mdb.connectWithConnectionString', MDB_OPEN_OVERVIEW_PAGE = 'mdb.openOverviewPage', MDB_DISCONNECT = 'mdb.disconnect', MDB_REMOVE_CONNECTION = 'mdb.removeConnection', diff --git a/src/connectionController.ts b/src/connectionController.ts index 13b17de3b..cc6c1c3aa 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -298,7 +298,8 @@ export default class ConnectionController { // Resolves false when it is added and not connected. // The connection can fail to connect but be successfully added. async addNewConnectionStringAndConnect( - connectionString: string + connectionString: string, + reuseExisting = false ): Promise { log.info('Trying to connect to a new connection configuration...'); @@ -312,13 +313,23 @@ export default class ConnectionController { ); try { - const connectResult = await this.saveNewConnectionAndConnect({ - connectionId: uuidv4(), - connectionOptions: { - connectionString: connectionStringData.toString(), - }, - connectionType: ConnectionTypes.CONNECTION_STRING, - }); + let existingId: string | undefined; + if (reuseExisting) { + existingId = this.getSavedConnections().find( + (connection) => + connection.connectionOptions?.connectionString === connectionString + )?.id; + } + + const connectResult = await (existingId + ? this.connectWithConnectionId(existingId) + : this.saveNewConnectionAndConnect({ + connectionId: uuidv4(), + connectionOptions: { + connectionString: connectionStringData.toString(), + }, + connectionType: ConnectionTypes.CONNECTION_STRING, + })); return connectResult.successfullyConnected; } catch (error) { diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index a671ada2d..31332b9b8 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -187,6 +187,7 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerCommands(); this.showOverviewPageIfRecentlyInstalled(); this.subscribeToConfigurationChanges(); + this.registerUriHandler(); const copilot = vscode.extensions.getExtension(COPILOT_EXTENSION_ID); void vscode.commands.executeCommand( @@ -210,6 +211,27 @@ export default class MDBExtensionController implements vscode.Disposable { } } + registerUriHandler = (): void => { + vscode.window.registerUriHandler({ + handleUri: async (uri: vscode.Uri): Promise => { + const command = uri.path.replace(/^\//, ''); + const parameters = uri.query.split('&').reduce((acc, param) => { + const [key, value] = param.split('='); + acc[key] = value; + return acc; + }, {} as Record); + + try { + await vscode.commands.executeCommand(command, parameters); + } catch (error) { + await vscode.window.showErrorMessage( + `Failed to handle '${uri}': ${error}` + ); + } + }, + }); + }; + registerCommands = (): void => { // Register our extension's commands. These are the event handlers and // control the functionality of our extension. @@ -222,6 +244,14 @@ export default class MDBExtensionController implements vscode.Disposable { this._webviewController.openWebview(this._context); return Promise.resolve(true); }); + this.registerCommand( + EXTENSION_COMMANDS.MDB_CONNECT_WITH_CONNECTION_STRING, + ({ connectionString, reuseExisting }) => + this._connectionController.addNewConnectionStringAndConnect( + connectionString, + reuseExisting + ) + ); this.registerCommand(EXTENSION_COMMANDS.MDB_CONNECT_WITH_URI, () => this._connectionController.connectWithURI() ); From 5085029fed1bf96ea9f10a8a8a37e40ca7eb6955 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 20 Mar 2025 13:59:52 +0100 Subject: [PATCH 02/11] chore: cleanup --- src/commands/index.ts | 1 - src/connectionController.ts | 14 +++++++++----- src/mdbExtensionController.ts | 26 +++++++++++++++----------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/commands/index.ts b/src/commands/index.ts index 935ebda25..348189649 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,7 +1,6 @@ enum EXTENSION_COMMANDS { MDB_CONNECT = 'mdb.connect', MDB_CONNECT_WITH_URI = 'mdb.connectWithURI', - MDB_CONNECT_WITH_CONNECTION_STRING = 'mdb.connectWithConnectionString', MDB_OPEN_OVERVIEW_PAGE = 'mdb.openOverviewPage', MDB_DISCONNECT = 'mdb.disconnect', MDB_REMOVE_CONNECTION = 'mdb.removeConnection', diff --git a/src/connectionController.ts b/src/connectionController.ts index cc6c1c3aa..3b34428c9 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -242,16 +242,17 @@ export default class ConnectionController { })); */ } - async connectWithURI(): Promise { - let connectionString: string | undefined; - + async connectWithURI( + connectionString?: string, + reuseExisting = false + ): Promise { log.info('connectWithURI command called'); const cancellationToken = new vscode.CancellationTokenSource(); this._connectionStringInputCancellationToken = cancellationToken; try { - connectionString = await vscode.window.showInputBox( + connectionString ??= await vscode.window.showInputBox( { value: '', ignoreFocusOut: true, @@ -291,7 +292,10 @@ export default class ConnectionController { return false; } - return this.addNewConnectionStringAndConnect(connectionString); + return this.addNewConnectionStringAndConnect( + connectionString, + reuseExisting + ); } // Resolves the new connection id when the connection is successfully added. diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 31332b9b8..6492a26dd 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -222,6 +222,15 @@ export default class MDBExtensionController implements vscode.Disposable { }, {} as Record); try { + if ( + !Object.values(EXTENSION_COMMANDS).includes( + command as EXTENSION_COMMANDS + ) + ) { + throw new Error( + `Unable to execute command '${command}' since it is not registered by the MongoDB extension.` + ); + } await vscode.commands.executeCommand(command, parameters); } catch (error) { await vscode.window.showErrorMessage( @@ -244,17 +253,12 @@ export default class MDBExtensionController implements vscode.Disposable { this._webviewController.openWebview(this._context); return Promise.resolve(true); }); - this.registerCommand( - EXTENSION_COMMANDS.MDB_CONNECT_WITH_CONNECTION_STRING, - ({ connectionString, reuseExisting }) => - this._connectionController.addNewConnectionStringAndConnect( - connectionString, - reuseExisting - ) - ); - this.registerCommand(EXTENSION_COMMANDS.MDB_CONNECT_WITH_URI, () => - this._connectionController.connectWithURI() - ); + this.registerCommand(EXTENSION_COMMANDS.MDB_CONNECT_WITH_URI, (params) => { + return this._connectionController.connectWithURI( + params?.connectionString, + params?.reuseExisting || false + ); + }); this.registerCommand(EXTENSION_COMMANDS.MDB_DISCONNECT, () => this._connectionController.disconnect() ); From c6dbc363166b117e8ab0de323275c593f0ed8e1d Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 20 Mar 2025 15:58:06 +0100 Subject: [PATCH 03/11] Use URLSearchParams --- src/mdbExtensionController.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 6492a26dd..e35761136 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -215,11 +215,7 @@ export default class MDBExtensionController implements vscode.Disposable { vscode.window.registerUriHandler({ handleUri: async (uri: vscode.Uri): Promise => { const command = uri.path.replace(/^\//, ''); - const parameters = uri.query.split('&').reduce((acc, param) => { - const [key, value] = param.split('='); - acc[key] = value; - return acc; - }, {} as Record); + const parameters = Object.fromEntries(new URLSearchParams(uri.query)); try { if ( From 4886e361b1ec4cf8bc519a1212871c2f459a2a2b Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 21 Mar 2025 12:55:22 +0100 Subject: [PATCH 04/11] add name support for new connection and remove by connection string --- package-lock.json | 51 ++++ package.json | 1 + src/connectionController.ts | 171 +++++++++----- src/mdbExtensionController.ts | 16 +- src/storage/connectionStorage.ts | 4 +- src/test/suite/connectionController.test.ts | 222 +++++++++--------- ...ygroundSelectionCodeActionProvider.test.ts | 2 +- .../suite/explorer/explorerController.test.ts | 42 ++-- src/test/suite/mdbExtensionController.test.ts | 12 +- src/test/suite/oidc.test.ts | 44 ++-- .../suite/views/webviewController.test.ts | 14 +- src/utils/types.ts | 1 + 12 files changed, 342 insertions(+), 238 deletions(-) create mode 100644 src/utils/types.ts diff --git a/package-lock.json b/package-lock.json index 64dc9304e..43e930887 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "mongodb-query-parser": "^4.3.0", "mongodb-schema": "^12.5.2", "numeral": "^2.0.6", + "query-string": "^9.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", "ts-log": "^2.2.7", @@ -12729,6 +12730,15 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, + "node_modules/decode-uri-component": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, "node_modules/decompress": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", @@ -15388,6 +15398,18 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -22490,6 +22512,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/query-string": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.1.tgz", + "integrity": "sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.4.1", + "filter-obj": "^5.1.0", + "split-on-first": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/querystring-es3": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", @@ -24036,6 +24075,18 @@ "spdx-ranges": "^2.0.0" } }, + "node_modules/split-on-first": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/package.json b/package.json index e1a06a205..a5a764364 100644 --- a/package.json +++ b/package.json @@ -1339,6 +1339,7 @@ "mongodb-query-parser": "^4.3.0", "mongodb-schema": "^12.5.2", "numeral": "^2.0.6", + "query-string": "^9.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", "ts-log": "^2.2.7", diff --git a/src/connectionController.ts b/src/connectionController.ts index 1be761910..69b04a414 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -31,6 +31,7 @@ import { isAtlasStream } from 'mongodb-build-info'; import type { ConnectionTreeItem } from './explorer'; import { PresetConnectionEditedTelemetryEvent } from './telemetry'; import getBuildInfo from 'mongodb-build-info'; +import type { RequiredBy } from './utils/types'; // eslint-disable-next-line @typescript-eslint/no-var-requires const packageJSON = require('../package.json'); @@ -73,6 +74,12 @@ type RecursivePartial = { : T[P]; }; +interface NewConnectionParams { + connectionString?: string; + name?: string; + reuseExisting?: boolean; +} + function isOIDCAuth(connectionString: string): boolean { const authMechanismString = ( new ConnectionString(connectionString).searchParams.get('authMechanism') || @@ -243,10 +250,11 @@ export default class ConnectionController { })); */ } - async connectWithURI( - connectionString?: string, - reuseExisting = false - ): Promise { + async connectWithURI({ + connectionString, + reuseExisting, + name, + }: NewConnectionParams = {}): Promise { log.info('connectWithURI command called'); const cancellationToken = new vscode.CancellationTokenSource(); @@ -293,19 +301,21 @@ export default class ConnectionController { return false; } - return this.addNewConnectionStringAndConnect( + return this.addNewConnectionStringAndConnect({ connectionString, - reuseExisting - ); + reuseExisting, + name, + }); } // Resolves the new connection id when the connection is successfully added. // Resolves false when it is added and not connected. // The connection can fail to connect but be successfully added. - async addNewConnectionStringAndConnect( - connectionString: string, - reuseExisting = false - ): Promise { + async addNewConnectionStringAndConnect({ + connectionString, + reuseExisting, + name, + }: RequiredBy): Promise { log.info('Trying to connect to a new connection configuration...'); const connectionStringData = new ConnectionString(connectionString); @@ -313,10 +323,7 @@ export default class ConnectionController { try { let existingId: string | undefined; if (reuseExisting) { - existingId = this.getSavedConnections().find( - (connection) => - connection.connectionOptions?.connectionString === connectionString - )?.id; + existingId = this._findConnectionIdByConnectionString(connectionString); } const connectResult = await (existingId @@ -327,6 +334,7 @@ export default class ConnectionController { connectionString: connectionStringData.toString(), }, connectionType: ConnectionTypes.CONNECTION_STRING, + name, })); return connectResult.successfullyConnected; @@ -354,14 +362,17 @@ export default class ConnectionController { connectionOptions, connectionId, connectionType, + name, }: { connectionOptions: ConnectionOptions; connectionId: string; connectionType: ConnectionTypes; + name?: string; }): Promise { const connection = this._connectionStorage.createNewConnection({ connectionId, connectionOptions, + name, }); await this._connectionStorage.saveConnection(connection); @@ -587,6 +598,15 @@ export default class ConnectionController { } } + _findConnectionIdByConnectionString( + connectionString: string + ): string | undefined { + return this.getSavedConnections().find( + (connection) => + connection.connectionOptions?.connectionString === connectionString + )?.id; + } + private async onConnectSuccess({ connectionInfo, dataService, @@ -759,23 +779,29 @@ export default class ConnectionController { } // Prompts the user to remove the connection then removes it on affirmation. - async removeMongoDBConnection(connectionId: string): Promise { - if (!this._connections[connectionId]) { + async removeMongoDBConnection( + connectionId: string, + promptForRemove = true + ): Promise { + const connection = this._connections[connectionId]; + if (!connection) { // No active connection(s) to remove. void vscode.window.showErrorMessage('Connection does not exist.'); return false; } - const removeConfirmationResponse = - await vscode.window.showInformationMessage( - `Are you sure to want to remove connection ${this._connections[connectionId].name}?`, - { modal: true }, - 'Yes' - ); + if (promptForRemove) { + const removeConfirmationResponse = + await vscode.window.showInformationMessage( + `Are you sure to want to remove connection ${connection.name}?`, + { modal: true }, + 'Yes' + ); - if (removeConfirmationResponse !== 'Yes') { - return false; + if (removeConfirmationResponse !== 'Yes') { + return false; + } } if (this._activeDataService && connectionId === this._currentConnectionId) { @@ -789,57 +815,78 @@ export default class ConnectionController { await this.removeSavedConnection(connectionId); - void vscode.window.showInformationMessage('MongoDB connection removed.'); + void vscode.window.showInformationMessage( + `MongoDB connection '${connection.name}' removed.` + ); return true; } - async onRemoveMongoDBConnection(): Promise { + async onRemoveMongoDBConnection({ + connectionString, + promptForRemove = true, + }: { + connectionString?: string; + promptForRemove?: boolean; + } = {}): Promise { log.info('mdb.removeConnection command called'); - const connectionIds = Object.entries(this._connections) - .map(([id, connection]) => { - return { id, connection }; - }) - .filter( - ({ connection }) => - connection.source !== 'globalSettings' && - connection.source !== 'workspaceSettings' - ); + let connectionIdToRemove: string; + if (connectionString) { + const connectionId = + this._findConnectionIdByConnectionString(connectionString); - if (connectionIds.length === 0) { - // No active connection(s) to remove. - void vscode.window.showErrorMessage('No connections to remove.'); + if (!connectionId) { + // No connection to remove, so just return silently. + return false; + } - return false; - } + connectionIdToRemove = connectionId; + } else { + const connectionIds = Object.entries(this._connections) + .map(([id, connection]) => { + return { id, connection }; + }) + .filter( + ({ connection }) => + connection.source !== 'globalSettings' && + connection.source !== 'workspaceSettings' + ); - if (connectionIds.length === 1) { - return this.removeMongoDBConnection(connectionIds[0].id); - } + if (connectionIds.length === 0) { + // No active connection(s) to remove. + void vscode.window.showErrorMessage('No connections to remove.'); - // There is more than 1 possible connection to remove. - // We attach the index of the connection so that we can infer their pick. - const connectionNameToRemove: string | undefined = - await vscode.window.showQuickPick( - connectionIds.map( - ({ connection }, index) => `${index + 1}: ${connection.name}` - ), - { - placeHolder: 'Choose a connection to remove...', + return false; + } + + if (connectionIds.length === 1) { + connectionIdToRemove = connectionIds[0].id; + } else { + // There is more than 1 possible connection to remove. + // We attach the index of the connection so that we can infer their pick. + const connectionNameToRemove: string | undefined = + await vscode.window.showQuickPick( + connectionIds.map( + ({ connection }, index) => `${index + 1}: ${connection.name}` + ), + { + placeHolder: 'Choose a connection to remove...', + } + ); + + if (!connectionNameToRemove) { + return false; } - ); - if (!connectionNameToRemove) { - return false; + // We attach the index of the connection so that we can infer their pick. + const connectionIndexToRemove = + Number(connectionNameToRemove.split(':', 1)[0]) - 1; + connectionIdToRemove = connectionIds[connectionIndexToRemove].id; + } } - // We attach the index of the connection so that we can infer their pick. - const connectionIndexToRemove = - Number(connectionNameToRemove.split(':', 1)[0]) - 1; - const connectionIdToRemove = connectionIds[connectionIndexToRemove].id; - - return this.removeMongoDBConnection(connectionIdToRemove); + return this.removeMongoDBConnection(connectionIdToRemove, promptForRemove); } async updateConnection({ diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index e35761136..e2dafaa7a 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -56,6 +56,8 @@ import { DocumentEditedTelemetryEvent, } from './telemetry'; +import queryString from 'query-string'; + // This class is the top-level controller for our extension. // Commands which the extensions handles are defined in the function `activate`. export default class MDBExtensionController implements vscode.Disposable { @@ -215,7 +217,10 @@ export default class MDBExtensionController implements vscode.Disposable { vscode.window.registerUriHandler({ handleUri: async (uri: vscode.Uri): Promise => { const command = uri.path.replace(/^\//, ''); - const parameters = Object.fromEntries(new URLSearchParams(uri.query)); + const parameters = queryString.parse(uri.query, { + parseBooleans: true, + parseNumbers: true, + }); try { if ( @@ -250,16 +255,13 @@ export default class MDBExtensionController implements vscode.Disposable { return Promise.resolve(true); }); this.registerCommand(EXTENSION_COMMANDS.MDB_CONNECT_WITH_URI, (params) => { - return this._connectionController.connectWithURI( - params?.connectionString, - params?.reuseExisting || false - ); + return this._connectionController.connectWithURI(params); }); this.registerCommand(EXTENSION_COMMANDS.MDB_DISCONNECT, () => this._connectionController.disconnect() ); - this.registerCommand(EXTENSION_COMMANDS.MDB_REMOVE_CONNECTION, () => - this._connectionController.onRemoveMongoDBConnection() + this.registerCommand(EXTENSION_COMMANDS.MDB_REMOVE_CONNECTION, (params) => + this._connectionController.onRemoveMongoDBConnection(params) ); this.registerCommand(EXTENSION_COMMANDS.MDB_CHANGE_ACTIVE_CONNECTION, () => this._connectionController.changeActiveConnection() diff --git a/src/storage/connectionStorage.ts b/src/storage/connectionStorage.ts index 0751cc3e0..838df6721 100644 --- a/src/storage/connectionStorage.ts +++ b/src/storage/connectionStorage.ts @@ -58,11 +58,13 @@ export class ConnectionStorage { createNewConnection({ connectionOptions, connectionId, + name, }: { connectionOptions: ConnectionOptions; connectionId: string; + name?: string; }): LoadedConnection { - const name = getConnectionTitle({ + name ??= getConnectionTitle({ connectionOptions, }); diff --git a/src/test/suite/connectionController.test.ts b/src/test/suite/connectionController.test.ts index 758eb63f5..fe9e88768 100644 --- a/src/test/suite/connectionController.test.ts +++ b/src/test/suite/connectionController.test.ts @@ -81,9 +81,9 @@ suite('Connection Controller Test Suite', function () { test('it connects to mongodb', async () => { const successfullyConnected = - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const connectionId = testConnectionController.getActiveConnectionId() || ''; const name = testConnectionController._connections[connectionId].name; const dataService = testConnectionController.getActiveDataService(); @@ -104,9 +104,9 @@ suite('Connection Controller Test Suite', function () { test('should append appName with connection and anonymous id', async () => { const successfullyConnected = - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const connectionId = testConnectionController.getActiveConnectionId() || ''; const connection = testConnectionController._connections[connectionId]; @@ -134,9 +134,9 @@ suite('Connection Controller Test Suite', function () { test('should override legacy appended appName and persist it', async () => { // Simulate legacy behavior of appending vscode appName by manually creating one const successfullyConnected = - await testConnectionController.addNewConnectionStringAndConnect( - `${TEST_DATABASE_URI}/?appname=mongodb-vscode+9.9.9` - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: `${TEST_DATABASE_URI}/?appname=mongodb-vscode+9.9.9`, + }); const connectionId = testConnectionController.getActiveConnectionId() || ''; @@ -171,9 +171,9 @@ suite('Connection Controller Test Suite', function () { test('does not override other user-set appName', async () => { const successfullyConnected = - await testConnectionController.addNewConnectionStringAndConnect( - `${TEST_DATABASE_URI}/?appName=test-123+9.9.9` - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: `${TEST_DATABASE_URI}/?appName=test-123+9.9.9`, + }); const connectionId = testConnectionController.getActiveConnectionId() || ''; let connection = testConnectionController._connections[connectionId]; @@ -204,9 +204,9 @@ suite('Connection Controller Test Suite', function () { }); test('getMongoClientConnectionOptions appends anonymous and connection ID and options properties', async function () { - await testConnectionController.addNewConnectionStringAndConnect( - `${TEST_DATABASE_URI}` - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const mongoClientConnectionOptions = testConnectionController.getMongoClientConnectionOptions(); @@ -242,9 +242,9 @@ suite('Connection Controller Test Suite', function () { test('"disconnect()" disconnects from the active connection', async () => { const successfullyConnected = - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); expect(successfullyConnected).to.be.true; expect(testConnectionController.getConnectionStatus()).to.equal( @@ -328,16 +328,16 @@ suite('Connection Controller Test Suite', function () { test('when adding a new connection it disconnects from the current connection', async () => { const succesfullyConnected = - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); expect(succesfullyConnected).to.be.true; try { - await testConnectionController.addNewConnectionStringAndConnect( - testDatabaseURI2WithTimeout - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: testDatabaseURI2WithTimeout, + }); } catch (error) { const expectedError = 'Failed to connect'; @@ -349,9 +349,9 @@ suite('Connection Controller Test Suite', function () { test('when adding a new connection it sets the connection controller as connecting while it disconnects from the current connection', async () => { const succesfullyConnected = - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); expect(succesfullyConnected).to.be.true; @@ -363,9 +363,9 @@ suite('Connection Controller Test Suite', function () { }); const succesfullyConnected2 = - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); expect(succesfullyConnected2).to.be.true; expect(wasSetToConnectingWhenDisconnecting).to.be.true; @@ -385,9 +385,9 @@ suite('Connection Controller Test Suite', function () { } ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); testConnectionController.removeEventListener( DataServiceEventTypes.CONNECTIONS_DID_CHANGE, @@ -412,9 +412,9 @@ suite('Connection Controller Test Suite', function () { } ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); await testConnectionController.disconnect(); testConnectionController.removeEventListener( @@ -446,9 +446,9 @@ suite('Connection Controller Test Suite', function () { }; // Should persist as this is a saved connection. - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); await testConnectionController.loadSavedConnections(); @@ -460,24 +460,24 @@ suite('Connection Controller Test Suite', function () { await vscode.workspace .getConfiguration('mdb.connectionSaving') .update('defaultConnectionSavingLocation', DefaultSavingLocations.Global); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); await vscode.workspace .getConfiguration('mdb.connectionSaving') .update( 'defaultConnectionSavingLocation', DefaultSavingLocations.Workspace ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); await testConnectionController.disconnect(); testConnectionController.clearAllConnections(); await testConnectionController.loadSavedConnections(); @@ -499,9 +499,9 @@ suite('Connection Controller Test Suite', function () { await vscode.workspace .getConfiguration('mdb.connectionSaving') .update('defaultConnectionSavingLocation', DefaultSavingLocations.Global); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const globalStoreConnections = testStorageController.get( StorageVariables.GLOBAL_SAVED_CONNECTIONS, @@ -531,9 +531,9 @@ suite('Connection Controller Test Suite', function () { 'defaultConnectionSavingLocation', DefaultSavingLocations.Workspace ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const workspaceStoreConnections = testStorageController.get( StorageVariables.WORKSPACE_SAVED_CONNECTIONS, @@ -582,9 +582,9 @@ suite('Connection Controller Test Suite', function () { 'defaultConnectionSavingLocation', DefaultSavingLocations.Workspace ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const workspaceStoreConnections = testStorageController.get( StorageVariables.WORKSPACE_SAVED_CONNECTIONS, @@ -618,9 +618,9 @@ suite('Connection Controller Test Suite', function () { const expectedDriverUrl = 'mongodb://localhost:27088/'; await testConnectionController.loadSavedConnections(); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const activeConnectionId = testConnectionController.getActiveConnectionId(); @@ -644,9 +644,9 @@ suite('Connection Controller Test Suite', function () { 'defaultConnectionSavingLocation', DefaultSavingLocations['Session Only'] ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const objectString = JSON.stringify(undefined); const globalStoreConnections = testStorageController.get( @@ -705,9 +705,9 @@ suite('Connection Controller Test Suite', function () { 'defaultConnectionSavingLocation', DefaultSavingLocations.Workspace ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const workspaceStoreConnections = testStorageController.get( StorageVariables.WORKSPACE_SAVED_CONNECTIONS, @@ -735,9 +735,9 @@ suite('Connection Controller Test Suite', function () { await vscode.workspace .getConfiguration('mdb.connectionSaving') .update('defaultConnectionSavingLocation', DefaultSavingLocations.Global); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const globalStoreConnections = testStorageController.get( StorageVariables.GLOBAL_SAVED_CONNECTIONS, @@ -764,9 +764,9 @@ suite('Connection Controller Test Suite', function () { 'deleteSecret' ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI_USER - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI_USER, + }); const [connection] = testConnectionController.getSavedConnections(); await testConnectionController.removeSavedConnection(connection.id); @@ -781,9 +781,9 @@ suite('Connection Controller Test Suite', function () { 'defaultConnectionSavingLocation', DefaultSavingLocations.Workspace ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const workspaceStoreConnections = testStorageController.get( StorageVariables.WORKSPACE_SAVED_CONNECTIONS, @@ -830,9 +830,9 @@ suite('Connection Controller Test Suite', function () { 'defaultConnectionSavingLocation', DefaultSavingLocations.Workspace ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const workspaceStoreConnections = testStorageController.get( StorageVariables.WORKSPACE_SAVED_CONNECTIONS, @@ -906,12 +906,12 @@ suite('Connection Controller Test Suite', function () { 'defaultConnectionSavingLocation', DefaultSavingLocations.Workspace ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); await testConnectionController.disconnect(); testConnectionController.clearAllConnections(); @@ -951,12 +951,12 @@ suite('Connection Controller Test Suite', function () { suite('connecting to a new connection when already connecting', () => { test('connects to the new connection', async () => { await Promise.all([ - testConnectionController.addNewConnectionStringAndConnect( - testDatabaseURI2WithTimeout - ), - testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ), + testConnectionController.addNewConnectionStringAndConnect({ + connectionString: testDatabaseURI2WithTimeout, + }), + testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }), ]); expect(testConnectionController.isConnecting()).to.be.false; @@ -1000,9 +1000,9 @@ suite('Connection Controller Test Suite', function () { suite('when connected', function () { beforeEach(async function () { - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); }); test('two disconnects on one connection at once complete without erroring', (done) => { @@ -1122,9 +1122,9 @@ suite('Connection Controller Test Suite', function () { await vscode.workspace .getConfiguration('mdb.connectionSaving') .update('defaultConnectionSavingLocation', DefaultSavingLocations.Global); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI_USER - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI_USER, + }); const workspaceStoreConnections = testStorageController.get( StorageVariables.GLOBAL_SAVED_CONNECTIONS @@ -1150,9 +1150,9 @@ suite('Connection Controller Test Suite', function () { }); test('getMongoClientConnectionOptions returns url and options properties', async () => { - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const mongoClientConnectionOptions = testConnectionController.getMongoClientConnectionOptions(); @@ -1218,12 +1218,12 @@ suite('Connection Controller Test Suite', function () { }); test('should be able to load connection with its secrets', async () => { - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI_USER - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI_USER, + }); // By default the connection secrets are already stored in SecretStorage const savedConnections = testConnectionController.getSavedConnections(); @@ -1252,9 +1252,9 @@ suite('Connection Controller Test Suite', function () { }); test('should fire a CONNECTIONS_DID_CHANGE event if connections are loaded successfully', async () => { - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI_USER - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI_USER, + }); await testConnectionController.disconnect(); testConnectionController.clearAllConnections(); diff --git a/src/test/suite/editors/playgroundSelectionCodeActionProvider.test.ts b/src/test/suite/editors/playgroundSelectionCodeActionProvider.test.ts index c3e3393cb..a4b8a7c22 100644 --- a/src/test/suite/editors/playgroundSelectionCodeActionProvider.test.ts +++ b/src/test/suite/editors/playgroundSelectionCodeActionProvider.test.ts @@ -44,7 +44,7 @@ suite('Playground Selection Code Action Provider Test Suite', function () { ); await mdbTestExtension.testExtensionController._connectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI + { connectionString: TEST_DATABASE_URI } ); const testExportToLanguageCodeLensProvider = diff --git a/src/test/suite/explorer/explorerController.test.ts b/src/test/suite/explorer/explorerController.test.ts index 0e47cced2..a91e46233 100644 --- a/src/test/suite/explorer/explorerController.test.ts +++ b/src/test/suite/explorer/explorerController.test.ts @@ -98,9 +98,9 @@ suite('Explorer Controller Test Suite', function () { const treeController = testExplorerController.getTreeController(); const succesfullyConnected = - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); assert( succesfullyConnected === true, @@ -144,9 +144,9 @@ suite('Explorer Controller Test Suite', function () { const treeController = testExplorerController.getTreeController(); const succesfullyConnected = - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); assert( succesfullyConnected === true, @@ -168,9 +168,9 @@ suite('Explorer Controller Test Suite', function () { assert(connectionsItemsFirstConnect[0].isExpanded); try { - await testConnectionController.addNewConnectionStringAndConnect( - testDatabaseURI2WithTimeout - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: testDatabaseURI2WithTimeout, + }); } catch (error) { /* Silent fail (should fail) */ } @@ -202,9 +202,9 @@ suite('Explorer Controller Test Suite', function () { mdbTestExtension.testExtensionController._explorerController; const treeController = testExplorerController.getTreeController(); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const connectionId = testConnectionController.getActiveConnectionId() || ''; @@ -258,9 +258,9 @@ suite('Explorer Controller Test Suite', function () { mdbTestExtension.testExtensionController._explorerController; const treeController = testExplorerController.getTreeController(); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const connectionsItems = await treeController.getChildren(); const databaseItems = await connectionsItems[0].getChildren(); @@ -282,9 +282,9 @@ suite('Explorer Controller Test Suite', function () { mdbTestExtension.testExtensionController._explorerController; const treeController = testExplorerController.getTreeController(); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); const connectionsItems = await treeController.getChildren(); @@ -355,9 +355,9 @@ suite('Explorer Controller Test Suite', function () { sandbox.replace(vscode.window, 'createTreeView', vscodeCreateTreeViewStub); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); await testConnectionController.disconnect(); assert(vscodeCreateTreeViewStub.called); diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index 518b917bc..2fd01d6ab 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -710,9 +710,9 @@ suite('MDBExtensionController Test Suite', function () { test.skip('mdb.dropCollection fails when a collection does not exist', async () => { const testConnectionController = mdbTestExtension.testExtensionController._connectionController; - await testConnectionController.addNewConnectionStringAndConnect( - testDatabaseURI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: testDatabaseURI, + }); const testCollectionTreeItem = getTestCollectionTreeItem({ collection: { @@ -802,9 +802,9 @@ suite('MDBExtensionController Test Suite', function () { test('mdb.dropDatabase succeeds even when a database doesnt exist (mdb behavior)', async () => { const testConnectionController = mdbTestExtension.testExtensionController._connectionController; - await testConnectionController.addNewConnectionStringAndConnect( - testDatabaseURI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: testDatabaseURI, + }); const testDatabaseTreeItem = getTestDatabaseTreeItem({ databaseName: 'narnia____a', diff --git a/src/test/suite/oidc.test.ts b/src/test/suite/oidc.test.ts index 29c853894..8830f4c9d 100644 --- a/src/test/suite/oidc.test.ts +++ b/src/test/suite/oidc.test.ts @@ -188,9 +188,9 @@ suite('OIDC Tests', function () { test('can successfully connect with a connection string', async function () { const succesfullyConnected = - await testConnectionController.addNewConnectionStringAndConnect( - connectionString - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString, + }); expect(succesfullyConnected).to.be.true; await launchMongoShell(testConnectionController); @@ -230,9 +230,9 @@ suite('OIDC Tests', function () { }; expect( - await testConnectionController.addNewConnectionStringAndConnect( - connectionString - ) + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString, + }) ).to.be.true; const connectionId = testConnectionController.getActiveConnectionId(); @@ -262,9 +262,9 @@ suite('OIDC Tests', function () { }; expect( - await testConnectionController.addNewConnectionStringAndConnect( - connectionString - ) + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString, + }) ).to.be.true; const connectionId = testConnectionController.getActiveConnectionId(); @@ -300,7 +300,7 @@ suite('OIDC Tests', function () { }; testConnectionController - .addNewConnectionStringAndConnect(connectionString) + .addNewConnectionStringAndConnect({ connectionString }) .catch(() => { // ignored }); @@ -308,9 +308,9 @@ suite('OIDC Tests', function () { await once(emitter, 'authorizeEndpointCalled'); overrideRequestHandler = (): void => {}; const connected = - await testConnectionController.addNewConnectionStringAndConnect( - connectionString - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString, + }); emitter.emit('secondConnectionEstablished'); expect(connected).to.be.true; }); @@ -346,9 +346,9 @@ suite('OIDC Tests', function () { }; expect( - await testConnectionController.addNewConnectionStringAndConnect( - connectionString - ) + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString, + }) ).to.be.true; afterReauth = true; @@ -395,9 +395,9 @@ suite('OIDC Tests', function () { }; const isConnected = - await testConnectionController.addNewConnectionStringAndConnect( - connectionString - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString, + }); expect(isConnected).to.be.true; @@ -433,9 +433,9 @@ suite('OIDC Tests', function () { }; expect( - await testConnectionController.addNewConnectionStringAndConnect( - connectionString - ) + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString, + }) ).to.be.true; await vscode.commands.executeCommand('mdb.createPlayground'); diff --git a/src/test/suite/views/webviewController.test.ts b/src/test/suite/views/webviewController.test.ts index ed8c33ae1..cec07a15a 100644 --- a/src/test/suite/views/webviewController.test.ts +++ b/src/test/suite/views/webviewController.test.ts @@ -356,9 +356,9 @@ suite('Webview Test Suite', () => { }, }); - void testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + void testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); }); test('web view runs the "connectWithURI" command when open connection string input is received', async () => { @@ -455,7 +455,7 @@ suite('Webview Test Suite', () => { ); void testConnectionController - .addNewConnectionStringAndConnect(TEST_DATABASE_URI) + .addNewConnectionStringAndConnect({ connectionString: TEST_DATABASE_URI }) .then(() => { // Mock a connection status request call. messageReceived({ @@ -492,9 +492,9 @@ suite('Webview Test Suite', () => { mdbTestExtension.extensionContextStub ); - await testConnectionController.addNewConnectionStringAndConnect( - TEST_DATABASE_URI - ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI, + }); // Mock a connection status request call. messageReceived({ diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 000000000..e2e1b2b8c --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1 @@ +export type RequiredBy = T & Required>; From 234c081d745f843d91ed9a252fc05677e3e4d535 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 21 Mar 2025 12:59:48 +0100 Subject: [PATCH 05/11] loosen command name requirements --- src/mdbExtensionController.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index e2dafaa7a..2ad0cd922 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -216,7 +216,11 @@ export default class MDBExtensionController implements vscode.Disposable { registerUriHandler = (): void => { vscode.window.registerUriHandler({ handleUri: async (uri: vscode.Uri): Promise => { - const command = uri.path.replace(/^\//, ''); + let command = uri.path.replace(/^\//, ''); + if (!command.startsWith('mdb.')) { + command = `mdb.${command}`; + } + const parameters = queryString.parse(uri.query, { parseBooleans: true, parseNumbers: true, From 8915182e798bdd2b24ea6edfdd4ef3530f3d2583 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 21 Mar 2025 13:27:50 +0100 Subject: [PATCH 06/11] use older query-string --- package-lock.json | 56 +++++++++++++++++++---------------- package.json | 2 +- src/mdbExtensionController.ts | 2 +- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 43e930887..f27506b1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "mongodb-query-parser": "^4.3.0", "mongodb-schema": "^12.5.2", "numeral": "^2.0.6", - "query-string": "^9.1.1", + "query-string": "^7.1.3", "react": "^18.3.1", "react-dom": "^18.3.1", "ts-log": "^2.2.7", @@ -12731,12 +12731,12 @@ "dev": true }, "node_modules/decode-uri-component": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", - "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "license": "MIT", "engines": { - "node": ">=14.16" + "node": ">=0.10" } }, "node_modules/decompress": { @@ -15399,15 +15399,12 @@ } }, "node_modules/filter-obj": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", - "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", "license": "MIT", "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, "node_modules/finalhandler": { @@ -22513,17 +22510,18 @@ } }, "node_modules/query-string": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.1.tgz", - "integrity": "sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", "license": "MIT", "dependencies": { - "decode-uri-component": "^0.4.1", - "filter-obj": "^5.1.0", - "split-on-first": "^3.0.0" + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" }, "engines": { - "node": ">=18" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -24076,15 +24074,12 @@ } }, "node_modules/split-on-first": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", - "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/sprintf-js": { @@ -24310,6 +24305,15 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", diff --git a/package.json b/package.json index a5a764364..bb774bb7e 100644 --- a/package.json +++ b/package.json @@ -1339,7 +1339,7 @@ "mongodb-query-parser": "^4.3.0", "mongodb-schema": "^12.5.2", "numeral": "^2.0.6", - "query-string": "^9.1.1", + "query-string": "^7.1.3", "react": "^18.3.1", "react-dom": "^18.3.1", "ts-log": "^2.2.7", diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 2ad0cd922..8e6c61c33 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -56,7 +56,7 @@ import { DocumentEditedTelemetryEvent, } from './telemetry'; -import queryString from 'query-string'; +import * as queryString from 'query-string'; // This class is the top-level controller for our extension. // Commands which the extensions handles are defined in the function `activate`. From 738ccfb446e93902979df86d720d257cd28916ae Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 21 Mar 2025 13:40:46 +0100 Subject: [PATCH 07/11] better argument names --- src/connectionController.ts | 22 ++++++++++++------- src/mdbExtensionController.ts | 4 +++- src/test/suite/mdbExtensionController.test.ts | 7 +++--- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/connectionController.ts b/src/connectionController.ts index 69b04a414..8d760af8c 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -779,10 +779,13 @@ export default class ConnectionController { } // Prompts the user to remove the connection then removes it on affirmation. - async removeMongoDBConnection( - connectionId: string, - promptForRemove = true - ): Promise { + async removeMongoDBConnection({ + connectionId, + force = false, + }: { + connectionId: string; + force?: boolean; + }): Promise { const connection = this._connections[connectionId]; if (!connection) { // No active connection(s) to remove. @@ -791,7 +794,7 @@ export default class ConnectionController { return false; } - if (promptForRemove) { + if (!force) { const removeConfirmationResponse = await vscode.window.showInformationMessage( `Are you sure to want to remove connection ${connection.name}?`, @@ -824,10 +827,10 @@ export default class ConnectionController { async onRemoveMongoDBConnection({ connectionString, - promptForRemove = true, + force = false, }: { connectionString?: string; - promptForRemove?: boolean; + force?: boolean; } = {}): Promise { log.info('mdb.removeConnection command called'); @@ -886,7 +889,10 @@ export default class ConnectionController { } } - return this.removeMongoDBConnection(connectionIdToRemove, promptForRemove); + return this.removeMongoDBConnection({ + connectionId: connectionIdToRemove, + force, + }); } async updateConnection({ diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 8e6c61c33..8b265f422 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -557,7 +557,9 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerCommand( EXTENSION_COMMANDS.MDB_REMOVE_CONNECTION_TREE_VIEW, (element: ConnectionTreeItem) => - this._connectionController.removeMongoDBConnection(element.connectionId) + this._connectionController.removeMongoDBConnection({ + connectionId: element.connectionId, + }) ); this.registerCommand( EXTENSION_COMMANDS.MDB_EDIT_CONNECTION, diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index 2fd01d6ab..8b916b925 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -312,10 +312,9 @@ suite('MDBExtensionController Test Suite', function () { testTreeItem ); assert.strictEqual(fakeRemoveMongoDBConnection.calledOnce, true); - assert.strictEqual( - fakeRemoveMongoDBConnection.firstCall.args[0], - 'craving_for_pancakes_with_maple_syrup' - ); + assert.deepStrictEqual(fakeRemoveMongoDBConnection.firstCall.args[0], { + connectionId: 'craving_for_pancakes_with_maple_syrup', + }); }); test('mdb.copyConnectionString command should try to copy the driver url to the vscode env clipboard', async () => { From 8d6de08d0e75d09b9114e0b920ef281b58cef1a4 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Mon, 24 Mar 2025 15:08:39 +0100 Subject: [PATCH 08/11] add tests --- src/connectionController.ts | 75 ++- src/mdbExtensionController.ts | 56 +- src/test/suite/connectionController.test.ts | 487 ++++++++++++++---- src/test/suite/mdbExtensionController.test.ts | 113 +++- 4 files changed, 587 insertions(+), 144 deletions(-) diff --git a/src/connectionController.ts b/src/connectionController.ts index 8d760af8c..f3b7896ad 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -303,7 +303,7 @@ export default class ConnectionController { return this.addNewConnectionStringAndConnect({ connectionString, - reuseExisting, + reuseExisting: reuseExisting ?? false, name, }); } @@ -601,9 +601,13 @@ export default class ConnectionController { _findConnectionIdByConnectionString( connectionString: string ): string | undefined { - return this.getSavedConnections().find( - (connection) => - connection.connectionOptions?.connectionString === connectionString + const searchStrings = [connectionString]; + if (!connectionString.endsWith('/')) { + searchStrings.push(`${connectionString}/`); + } + + return this.getConnectionsFromHistory().find((connection) => + searchStrings.includes(connection.connectionOptions?.connectionString) )?.id; } @@ -779,7 +783,7 @@ export default class ConnectionController { } // Prompts the user to remove the connection then removes it on affirmation. - async removeMongoDBConnection({ + async _removeMongoDBConnection({ connectionId, force = false, }: { @@ -825,20 +829,36 @@ export default class ConnectionController { return true; } - async onRemoveMongoDBConnection({ - connectionString, - force = false, - }: { - connectionString?: string; - force?: boolean; - } = {}): Promise { + async onRemoveMongoDBConnection( + options: ( + | { connectionString: string } + | { connectionName: string } + | { connectionId: string } + | {} + ) & { + force?: boolean; + } = {} + ): Promise { log.info('mdb.removeConnection command called'); let connectionIdToRemove: string; - if (connectionString) { - const connectionId = - this._findConnectionIdByConnectionString(connectionString); + if ('connectionId' in options) { + connectionIdToRemove = options.connectionId; + } else if ('connectionString' in options) { + const connectionId = this._findConnectionIdByConnectionString( + options.connectionString + ); + + if (!connectionId) { + // No connection to remove, so just return silently. + return false; + } + connectionIdToRemove = connectionId; + } else if ('connectionName' in options) { + const connectionId = this.getConnectionsFromHistory().find( + (connection) => connection.name === options.connectionName + )?.id; if (!connectionId) { // No connection to remove, so just return silently. return false; @@ -846,15 +866,7 @@ export default class ConnectionController { connectionIdToRemove = connectionId; } else { - const connectionIds = Object.entries(this._connections) - .map(([id, connection]) => { - return { id, connection }; - }) - .filter( - ({ connection }) => - connection.source !== 'globalSettings' && - connection.source !== 'workspaceSettings' - ); + const connectionIds = this.getConnectionsFromHistory(); if (connectionIds.length === 0) { // No active connection(s) to remove. @@ -871,7 +883,7 @@ export default class ConnectionController { const connectionNameToRemove: string | undefined = await vscode.window.showQuickPick( connectionIds.map( - ({ connection }, index) => `${index + 1}: ${connection.name}` + (connection, index) => `${index + 1}: ${connection.name}` ), { placeHolder: 'Choose a connection to remove...', @@ -889,9 +901,9 @@ export default class ConnectionController { } } - return this.removeMongoDBConnection({ + return this._removeMongoDBConnection({ connectionId: connectionIdToRemove, - force, + force: options.force, }); } @@ -1007,6 +1019,15 @@ export default class ConnectionController { return Object.values(this._connections); } + private getConnectionsFromHistory(): LoadedConnection[] { + return this.getSavedConnections().filter((connection) => { + return ( + connection.source !== 'globalSettings' && + connection.source !== 'workspaceSettings' + ); + }); + } + getSavedConnectionName(connectionId: string): string { return this._connections[connectionId] ? this._connections[connectionId].name diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 8b265f422..c88a61508 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -215,35 +215,37 @@ export default class MDBExtensionController implements vscode.Disposable { registerUriHandler = (): void => { vscode.window.registerUriHandler({ - handleUri: async (uri: vscode.Uri): Promise => { - let command = uri.path.replace(/^\//, ''); - if (!command.startsWith('mdb.')) { - command = `mdb.${command}`; - } + handleUri: this._handleDeepLink, + }); + }; - const parameters = queryString.parse(uri.query, { - parseBooleans: true, - parseNumbers: true, - }); + _handleDeepLink = async (uri: vscode.Uri): Promise => { + let command = uri.path.replace(/^\//, ''); + if (!command.startsWith('mdb.')) { + command = `mdb.${command}`; + } - try { - if ( - !Object.values(EXTENSION_COMMANDS).includes( - command as EXTENSION_COMMANDS - ) - ) { - throw new Error( - `Unable to execute command '${command}' since it is not registered by the MongoDB extension.` - ); - } - await vscode.commands.executeCommand(command, parameters); - } catch (error) { - await vscode.window.showErrorMessage( - `Failed to handle '${uri}': ${error}` - ); - } - }, + const parameters = queryString.parse(uri.query, { + parseBooleans: true, + parseNumbers: true, }); + + try { + if ( + !Object.values(EXTENSION_COMMANDS).includes( + command as EXTENSION_COMMANDS + ) + ) { + throw new Error( + `Unable to execute command '${command}' since it is not registered by the MongoDB extension.` + ); + } + await vscode.commands.executeCommand(command, parameters); + } catch (error) { + await vscode.window.showErrorMessage( + `Failed to handle '${uri}': ${error}` + ); + } }; registerCommands = (): void => { @@ -557,7 +559,7 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerCommand( EXTENSION_COMMANDS.MDB_REMOVE_CONNECTION_TREE_VIEW, (element: ConnectionTreeItem) => - this._connectionController.removeMongoDBConnection({ + this._connectionController.onRemoveMongoDBConnection({ connectionId: element.connectionId, }) ); diff --git a/src/test/suite/connectionController.test.ts b/src/test/suite/connectionController.test.ts index fe9e88768..dd364b3b2 100644 --- a/src/test/suite/connectionController.test.ts +++ b/src/test/suite/connectionController.test.ts @@ -8,6 +8,7 @@ import { expect } from 'chai'; import ConnectionString from 'mongodb-connection-string-url'; import ConnectionController, { + ConnectionTypes, DataServiceEventTypes, getNotifyDeviceFlowForConnectionAttempt, } from '../../connectionController'; @@ -270,60 +271,180 @@ suite('Connection Controller Test Suite', function () { expect(dataService).to.be.null; }); - test('"removeMongoDBConnection()" returns a reject promise when there is no active connection', async () => { - const expectedMessage = 'No connections to remove.'; - const successfullyRemovedMongoDBConnection = - await testConnectionController.onRemoveMongoDBConnection(); + suite('onRemoveMongoDBConnection', () => { + const addConnection = ( + id: string, + name: string, + connectionString = 'mongodb://localhost:12345', + otherOptions: Partial = {} + ): void => { + testConnectionController._connections[id] = { + connectionOptions: { connectionString }, + storageLocation: StorageLocation.NONE, + secretStorageLocation: SecretStorageLocation.SecretStorage, + name, + id, + ...otherOptions, + }; + }; - expect(showErrorMessageStub.firstCall.args[0]).to.equal(expectedMessage); - expect(successfullyRemovedMongoDBConnection).to.be.false; - }); + test('returns a reject promise when there is no active connection', async () => { + const expectedMessage = 'No connections to remove.'; + const successfullyRemovedMongoDBConnection = + await testConnectionController.onRemoveMongoDBConnection(); - test('"removeMongoDBConnection()" hides preset connections', async () => { - const connectionBase: Omit = { - connectionOptions: { - connectionString: 'localhost:3000', - }, - storageLocation: StorageLocation.NONE, - secretStorageLocation: SecretStorageLocation.SecretStorage, - }; + expect(showErrorMessageStub.firstCall.args[0]).to.equal(expectedMessage); + expect(successfullyRemovedMongoDBConnection).to.be.false; + }); - testConnectionController._connections['1234'] = { - ...connectionBase, - name: 'valid 1', - id: '1234', - }; - testConnectionController._connections['5678'] = { - ...connectionBase, - name: 'valid 2', - id: '5678', - source: 'user', - }; - testConnectionController._connections['3333'] = { - ...connectionBase, - id: '3333', - name: 'invalid 1', - source: 'workspaceSettings', - }; - testConnectionController._connections['3333'] = { - ...connectionBase, - id: '3333', - name: 'invalid 2', - source: 'globalSettings', - }; + test('hides preset connections', async () => { + addConnection('1234', 'valid 1'); + addConnection('5678', 'valid 2', undefined, { source: 'user' }); + addConnection('3333', 'invalid 1', undefined, { + source: 'workspaceSettings', + }); + addConnection('4444', 'invalid 2', undefined, { + source: 'globalSettings', + }); - const showQuickPickStub = sinon - .stub(vscode.window, 'showQuickPick') - .resolves(undefined); - const successfullyRemovedMongoDBConnection = - await testConnectionController.onRemoveMongoDBConnection(); + const showQuickPickStub = sinon + .stub(vscode.window, 'showQuickPick') + .resolves(undefined); + const successfullyRemovedMongoDBConnection = + await testConnectionController.onRemoveMongoDBConnection(); - expect(showErrorMessageStub).not.called; - expect(showQuickPickStub.firstCall.firstArg).deep.equal([ - '1: valid 1', - '2: valid 2', - ]); - expect(successfullyRemovedMongoDBConnection).to.be.false; + expect(showErrorMessageStub).not.called; + expect(showQuickPickStub.firstCall.firstArg).deep.equal([ + '1: valid 1', + '2: valid 2', + ]); + expect(successfullyRemovedMongoDBConnection).to.be.false; + }); + + test('when connection does not exist, shows error', async () => { + const didRemove = + await testConnectionController.onRemoveMongoDBConnection({ + connectionId: 'abc', + }); + expect(didRemove).to.be.false; + expect(showErrorMessageStub).to.be.calledOnceWith( + 'Connection does not exist.' + ); + }); + + test('when force: false, prompts user for confirmation', async () => { + addConnection('1234', 'foo'); + showInformationMessageStub.resolves('No'); + + const didRemove = + await testConnectionController.onRemoveMongoDBConnection({ + connectionId: '1234', + }); + + expect(didRemove).to.be.false; + expect(showInformationMessageStub).to.be.calledOnceWith( + 'Are you sure to want to remove connection foo?', + { modal: true }, + 'Yes' + ); + }); + + test('when force: true, does not prompt user for confirmation', async () => { + addConnection('1234', 'foo'); + + const didRemove = + await testConnectionController.onRemoveMongoDBConnection({ + connectionId: '1234', + force: true, + }); + + expect(didRemove).to.be.true; + expect(testConnectionController._connections['1234']).to.be.undefined; + }); + + test('with connection name, removes connection', async () => { + addConnection('1234', 'bar'); + + const didRemove = + await testConnectionController.onRemoveMongoDBConnection({ + connectionName: 'bar', + force: true, + }); + + expect(didRemove).to.be.true; + expect(testConnectionController._connections['1234']).to.be.undefined; + }); + + test('with connection name, when not found, silently returns', async () => { + addConnection('1234', 'bar'); + + const didRemove = + await testConnectionController.onRemoveMongoDBConnection({ + connectionName: 'foo', + force: true, + }); + + expect(didRemove).to.be.false; + expect(showInformationMessageStub).to.not.have.been.called; + expect(testConnectionController._connections['1234']).to.not.be.undefined; + }); + + test('with connection name, when multiple connections match, removes first one', async () => { + addConnection('1234', 'bar'); + addConnection('5678', 'bar'); + + const didRemove = + await testConnectionController.onRemoveMongoDBConnection({ + connectionName: 'bar', + force: true, + }); + + expect(didRemove).to.be.true; + expect(testConnectionController._connections['1234']).to.be.undefined; + expect(testConnectionController._connections['5678']).to.not.be.undefined; + }); + + test('with connection string, removes connection', async () => { + addConnection('1234', 'bar', 'mongodb://localhost:12345'); + + const didRemove = + await testConnectionController.onRemoveMongoDBConnection({ + connectionString: 'mongodb://localhost:12345', + force: true, + }); + + expect(didRemove).to.be.true; + expect(testConnectionController._connections['1234']).to.be.undefined; + }); + + test('with connection name, when not found, silently returns', async () => { + addConnection('1234', 'bar', 'mongodb://localhost:12345'); + + const didRemove = + await testConnectionController.onRemoveMongoDBConnection({ + connectionString: 'mongodb://localhost:27017', + force: true, + }); + + expect(didRemove).to.be.false; + expect(showInformationMessageStub).to.not.have.been.called; + expect(testConnectionController._connections['1234']).to.not.be.undefined; + }); + + test('with connection name, when multiple connections match, removes first one', async () => { + addConnection('1234', 'foo', 'mongodb://localhost:12345'); + addConnection('5678', 'bar', 'mongodb://localhost:12345'); + + const didRemove = + await testConnectionController.onRemoveMongoDBConnection({ + connectionString: 'mongodb://localhost:12345', + force: true, + }); + + expect(didRemove).to.be.true; + expect(testConnectionController._connections['1234']).to.be.undefined; + expect(testConnectionController._connections['5678']).to.not.be.undefined; + }); }); test('when adding a new connection it disconnects from the current connection', async () => { @@ -1113,42 +1234,6 @@ suite('Connection Controller Test Suite', function () { expect(newSavedConnectionInfoWithSecrets).to.deep.equal(connectionInfo); }); - test('addNewConnectionStringAndConnect saves connection without secrets to the global storage', async () => { - const fakeConnect = sandbox.fake.resolves({ - successfullyConnected: true, - }); - sandbox.replace(testConnectionController, '_connect', fakeConnect); - - await vscode.workspace - .getConfiguration('mdb.connectionSaving') - .update('defaultConnectionSavingLocation', DefaultSavingLocations.Global); - await testConnectionController.addNewConnectionStringAndConnect({ - connectionString: TEST_DATABASE_URI_USER, - }); - - const workspaceStoreConnections = testStorageController.get( - StorageVariables.GLOBAL_SAVED_CONNECTIONS - ); - - expect(workspaceStoreConnections).to.not.be.empty; - const connections = Object.values(workspaceStoreConnections); - - expect(connections).to.have.lengthOf(1); - expect(connections[0].connectionOptions?.connectionString).to.include( - TEST_USER_USERNAME - ); - expect(connections[0].connectionOptions?.connectionString).to.not.include( - TEST_USER_PASSWORD - ); - expect( - testConnectionController._connections[connections[0].id].connectionOptions - ?.connectionString - ).to.include(TEST_USER_PASSWORD); - expect( - testConnectionController._connections[connections[0].id].name - ).to.equal('localhost:27088'); - }); - test('getMongoClientConnectionOptions returns url and options properties', async () => { await testConnectionController.addNewConnectionStringAndConnect({ connectionString: TEST_DATABASE_URI, @@ -1213,10 +1298,6 @@ suite('Connection Controller Test Suite', function () { }); suite('when connection secrets are already in SecretStorage', () => { - afterEach(() => { - testSandbox.restore(); - }); - test('should be able to load connection with its secrets', async () => { await testConnectionController.addNewConnectionStringAndConnect({ connectionString: TEST_DATABASE_URI, @@ -1419,4 +1500,232 @@ suite('Connection Controller Test Suite', function () { ]); }); }); + + suite('connectWithURI', () => { + let showInputBoxStub: sinon.SinonStub; + let addNewConnectionAndConnectStub: sinon.SinonStub; + + beforeEach(() => { + showInputBoxStub = sandbox.stub(vscode.window, 'showInputBox'); + addNewConnectionAndConnectStub = sandbox.stub( + testConnectionController, + 'addNewConnectionStringAndConnect' + ); + }); + + test('without arguments, prompts for input', async () => { + showInputBoxStub.returns(undefined); + + const result = await testConnectionController.connectWithURI(); + expect(result).to.be.false; + expect(showInputBoxStub).to.have.been.calledOnce; + }); + + test('without arguments, uses input provided by user', async () => { + showInputBoxStub.returns(TEST_DATABASE_URI); + addNewConnectionAndConnectStub.returns(true); + + const result = await testConnectionController.connectWithURI(); + expect(result).to.be.true; + expect(showInputBoxStub).to.have.been.calledOnce; + expect(addNewConnectionAndConnectStub).to.have.been.calledOnceWithExactly( + { + connectionString: TEST_DATABASE_URI, + reuseExisting: false, + name: undefined, + } + ); + }); + + test('with arguments, uses provided connection string', async () => { + addNewConnectionAndConnectStub.returns(true); + + const result = await testConnectionController.connectWithURI({ + connectionString: 'mongodb://127.0.0.1:12345', + reuseExisting: true, + name: 'foo', + }); + expect(result).to.be.true; + expect(showInputBoxStub).to.not.have.been.called; + expect(addNewConnectionAndConnectStub).to.have.been.calledOnceWithExactly( + { + connectionString: 'mongodb://127.0.0.1:12345', + reuseExisting: true, + name: 'foo', + } + ); + }); + }); + + suite('addNewConnectionStringAndConnect', () => { + let fakeConnect: sinon.SinonStub; + + beforeEach(() => { + fakeConnect = sandbox + .stub(testConnectionController, '_connect') + .resolves({ successfullyConnected: true, connectionErrorMessage: '' }); + }); + + test('saves connection without secrets to the global storage', async () => { + await vscode.workspace + .getConfiguration('mdb.connectionSaving') + .update( + 'defaultConnectionSavingLocation', + DefaultSavingLocations.Global + ); + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI_USER, + }); + + const workspaceStoreConnections = testStorageController.get( + StorageVariables.GLOBAL_SAVED_CONNECTIONS + ); + + expect(workspaceStoreConnections).to.not.be.empty; + const connections = Object.values(workspaceStoreConnections); + + expect(connections).to.have.lengthOf(1); + expect(connections[0].connectionOptions?.connectionString).to.include( + TEST_USER_USERNAME + ); + expect(connections[0].connectionOptions?.connectionString).to.not.include( + TEST_USER_PASSWORD + ); + expect( + testConnectionController._connections[connections[0].id] + .connectionOptions?.connectionString + ).to.include(TEST_USER_PASSWORD); + expect( + testConnectionController._connections[connections[0].id].name + ).to.equal('localhost:27088'); + }); + + suite('with reuseExisting: ', () => { + test('false, adds a new connection', async () => { + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI_USER, + name: 'foo', + reuseExisting: false, + }); + + expect(testConnectionController.getSavedConnections()).to.have.lengthOf( + 1 + ); + + expect(fakeConnect).to.have.been.calledOnce; + + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI_USER, + name: 'foo', + reuseExisting: false, + }); + + expect(testConnectionController.getSavedConnections()).to.have.lengthOf( + 2 + ); + expect(fakeConnect).to.have.been.calledTwice; + }); + + test('true, reuses existing connection', async () => { + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI_USER, + name: 'foo', + reuseExisting: true, + }); + + expect(testConnectionController.getSavedConnections()).to.have.lengthOf( + 1 + ); + + expect(fakeConnect).to.have.been.calledOnce; + + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI_USER, + name: 'bar', + reuseExisting: true, + }); + + expect(testConnectionController.getSavedConnections()).to.have.lengthOf( + 1 + ); + expect(fakeConnect).to.have.been.calledTwice; + }); + + test('true, does not override existing connection name', async () => { + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI_USER, + name: 'foo', + reuseExisting: true, + }); + + let connections = testConnectionController.getSavedConnections(); + expect(connections).to.have.lengthOf(1); + expect(connections[0].name).to.equal('foo'); + + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: TEST_DATABASE_URI_USER, + name: 'bar', + reuseExisting: true, + }); + + connections = testConnectionController.getSavedConnections(); + expect(connections).to.have.lengthOf(1); + expect(connections[0].name).to.equal('foo'); // not 'bar' + }); + + test('true, matches connection regardless of trailing slash', async () => { + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: 'mongodb://localhost:12345/', + reuseExisting: true, + }); + + let connections = testConnectionController.getSavedConnections(); + expect(connections[0].connectionOptions?.connectionString).to.equal( + 'mongodb://localhost:12345/' + ); + + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: 'mongodb://localhost:12345/', + reuseExisting: true, + }); + expect(fakeConnect).to.have.been.calledWith( + connections[0].id, + ConnectionTypes.CONNECTION_ID + ); + connections = testConnectionController.getSavedConnections(); + expect(connections).to.have.lengthOf(1); + + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: 'mongodb://localhost:12345', // No-slash + reuseExisting: true, + }); + expect(fakeConnect).to.have.been.calledWith(connections[0].id); + connections = testConnectionController.getSavedConnections(); + expect(connections).to.have.lengthOf(1); + }); + }); + + suite('with name: ', () => { + test('supplied, uses provided name', async () => { + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: 'mongodb://localhost:12345/', + name: 'foo', + }); + + const connections = testConnectionController.getSavedConnections(); + expect(connections).to.have.lengthOf(1); + expect(connections[0].name).to.equal('foo'); + }); + + test('not supplied, generates one', async () => { + await testConnectionController.addNewConnectionStringAndConnect({ + connectionString: 'mongodb://localhost:12345/', + }); + + const connections = testConnectionController.getSavedConnections(); + expect(connections).to.have.lengthOf(1); + expect(connections[0].name).to.equal('localhost:12345'); + }); + }); + }); }); diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index 8b916b925..0b8e2eecf 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -28,6 +28,7 @@ import { } from '../../storage/storageController'; import { VIEW_COLLECTION_SCHEME } from '../../editors/collectionDocumentsProvider'; import type { CollectionDetailsType } from '../../explorer/collectionTreeItem'; +import { expect } from 'chai'; const testDatabaseURI = 'mongodb://localhost:27088'; @@ -304,7 +305,7 @@ suite('MDBExtensionController Test Suite', function () { const fakeRemoveMongoDBConnection = sandbox.fake(); sandbox.replace( mdbTestExtension.testExtensionController._connectionController, - 'removeMongoDBConnection', + '_removeMongoDBConnection', fakeRemoveMongoDBConnection ); await vscode.commands.executeCommand( @@ -1765,4 +1766,114 @@ suite('MDBExtensionController Test Suite', function () { preview: true, }); }); + + suite('handleDeepLink', () => { + let fakeExecuteCommand: sinon.SinonStub; + + let fakeShowErrorMessage: sinon.SinonSpy; + + beforeEach(() => { + fakeExecuteCommand = sandbox.stub(vscode.commands, 'executeCommand'); + fakeShowErrorMessage = sandbox.stub(vscode.window, 'showErrorMessage'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + test('errors when command is not registered', async () => { + await mdbTestExtension.testExtensionController._handleDeepLink( + vscode.Uri.parse('vscode://mongodb.mongodb-vscode/invalid-command') + ); + + expect(fakeExecuteCommand).to.not.have.been.called; + expect(fakeShowErrorMessage).to.have.been.calledOnceWith( + "Failed to handle 'vscode://mongodb.mongodb-vscode/invalid-command': Error: Unable to execute command 'mdb.invalid-command' since it is not registered by the MongoDB extension." + ); + }); + + test('handles valid command', async () => { + await mdbTestExtension.testExtensionController._handleDeepLink( + vscode.Uri.parse('vscode://mongodb.mongodb-vscode/mdb.connectWithURI') + ); + + expect(fakeExecuteCommand).to.have.been.calledWith('mdb.connectWithURI'); + expect(fakeShowErrorMessage).to.not.have.been.called; + }); + + test('handles valid command without mdb. prefix', async () => { + await mdbTestExtension.testExtensionController._handleDeepLink( + vscode.Uri.parse('vscode://mongodb.mongodb-vscode/connectWithURI') + ); + + expect(fakeExecuteCommand).to.have.been.calledWith('mdb.connectWithURI'); + expect(fakeShowErrorMessage).to.not.have.been.called; + }); + + test('handles valid command with query parameters', async () => { + await mdbTestExtension.testExtensionController._handleDeepLink( + vscode.Uri.parse( + 'vscode://mongodb.mongodb-vscode/connectWithURI?foo=bar&baz=qux' + ) + ); + + expect(fakeExecuteCommand).to.have.been.calledWith('mdb.connectWithURI', { + foo: 'bar', + baz: 'qux', + }); + expect(fakeShowErrorMessage).to.not.have.been.called; + }); + + test('converts query parameters to booleans and numbers', async () => { + await mdbTestExtension.testExtensionController._handleDeepLink( + vscode.Uri.parse( + 'vscode://mongodb.mongodb-vscode/connectWithURI?foo=true&bar=987&baz=str' + ) + ); + + expect(fakeExecuteCommand).to.have.been.calledOnceWith( + 'mdb.connectWithURI', + { + foo: true, + bar: 987, + baz: 'str', + } + ); + expect(fakeShowErrorMessage).to.not.have.been.called; + }); + + test('decodes query parameters', async () => { + await mdbTestExtension.testExtensionController._handleDeepLink( + vscode.Uri.from({ + scheme: 'vscode', + authority: 'mongodb.mongodb-vscode', + path: '/connectWithURI', + query: + 'connectionString=mongodb%3A%2F%2Flocalhost%3A27017%2F%3FappName%3Dblah%26test%3Dtrue&reuseExisting=true', + }) + ); + + expect(fakeExecuteCommand).to.have.been.calledOnceWith( + 'mdb.connectWithURI', + { + connectionString: 'mongodb://localhost:27017/?appName=blah&test=true', + reuseExisting: true, + } + ); + expect(fakeShowErrorMessage).to.not.have.been.called; + }); + + test('shows an error message when executeCommand fails', async () => { + fakeExecuteCommand.rejects(new Error('fake error')); + + await mdbTestExtension.testExtensionController._handleDeepLink( + vscode.Uri.parse('vscode://mongodb.mongodb-vscode/mdb.connectWithURI') + ); + + expect(fakeExecuteCommand).to.have.been.calledWith('mdb.connectWithURI'); + expect(fakeShowErrorMessage).to.have.been.calledOnceWith( + "Failed to handle 'vscode://mongodb.mongodb-vscode/mdb.connectWithURI': Error: fake error" + ); + }); + }); }); From dd08569dde288b5370e724b711032d0d67681531 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Mon, 24 Mar 2025 15:24:09 +0100 Subject: [PATCH 09/11] fix test condition --- src/test/suite/mdbExtensionController.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index 0b8e2eecf..1cb3e6c96 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -315,6 +315,7 @@ suite('MDBExtensionController Test Suite', function () { assert.strictEqual(fakeRemoveMongoDBConnection.calledOnce, true); assert.deepStrictEqual(fakeRemoveMongoDBConnection.firstCall.args[0], { connectionId: 'craving_for_pancakes_with_maple_syrup', + force: undefined, }); }); From 95bbdf015e3b456950c0892e0cc8169f95eedbb4 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Mon, 24 Mar 2025 15:26:30 +0100 Subject: [PATCH 10/11] simplify naming --- src/connectionController.ts | 12 ++++++------ src/mdbExtensionController.ts | 2 +- src/test/suite/connectionController.test.ts | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/connectionController.ts b/src/connectionController.ts index f3b7896ad..37785ddde 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -832,8 +832,8 @@ export default class ConnectionController { async onRemoveMongoDBConnection( options: ( | { connectionString: string } - | { connectionName: string } - | { connectionId: string } + | { name: string } + | { id: string } | {} ) & { force?: boolean; @@ -842,8 +842,8 @@ export default class ConnectionController { log.info('mdb.removeConnection command called'); let connectionIdToRemove: string; - if ('connectionId' in options) { - connectionIdToRemove = options.connectionId; + if ('id' in options) { + connectionIdToRemove = options.id; } else if ('connectionString' in options) { const connectionId = this._findConnectionIdByConnectionString( options.connectionString @@ -855,9 +855,9 @@ export default class ConnectionController { } connectionIdToRemove = connectionId; - } else if ('connectionName' in options) { + } else if ('name' in options) { const connectionId = this.getConnectionsFromHistory().find( - (connection) => connection.name === options.connectionName + (connection) => connection.name === options.name )?.id; if (!connectionId) { // No connection to remove, so just return silently. diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index c88a61508..5613a43d9 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -560,7 +560,7 @@ export default class MDBExtensionController implements vscode.Disposable { EXTENSION_COMMANDS.MDB_REMOVE_CONNECTION_TREE_VIEW, (element: ConnectionTreeItem) => this._connectionController.onRemoveMongoDBConnection({ - connectionId: element.connectionId, + id: element.connectionId, }) ); this.registerCommand( diff --git a/src/test/suite/connectionController.test.ts b/src/test/suite/connectionController.test.ts index dd364b3b2..fd5f7acf8 100644 --- a/src/test/suite/connectionController.test.ts +++ b/src/test/suite/connectionController.test.ts @@ -324,7 +324,7 @@ suite('Connection Controller Test Suite', function () { test('when connection does not exist, shows error', async () => { const didRemove = await testConnectionController.onRemoveMongoDBConnection({ - connectionId: 'abc', + id: 'abc', }); expect(didRemove).to.be.false; expect(showErrorMessageStub).to.be.calledOnceWith( @@ -338,7 +338,7 @@ suite('Connection Controller Test Suite', function () { const didRemove = await testConnectionController.onRemoveMongoDBConnection({ - connectionId: '1234', + id: '1234', }); expect(didRemove).to.be.false; @@ -354,7 +354,7 @@ suite('Connection Controller Test Suite', function () { const didRemove = await testConnectionController.onRemoveMongoDBConnection({ - connectionId: '1234', + id: '1234', force: true, }); @@ -367,7 +367,7 @@ suite('Connection Controller Test Suite', function () { const didRemove = await testConnectionController.onRemoveMongoDBConnection({ - connectionName: 'bar', + name: 'bar', force: true, }); @@ -380,7 +380,7 @@ suite('Connection Controller Test Suite', function () { const didRemove = await testConnectionController.onRemoveMongoDBConnection({ - connectionName: 'foo', + name: 'foo', force: true, }); @@ -395,7 +395,7 @@ suite('Connection Controller Test Suite', function () { const didRemove = await testConnectionController.onRemoveMongoDBConnection({ - connectionName: 'bar', + name: 'bar', force: true, }); From e65315c72387a44bc8e4beba765e55ad2024a82b Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 25 Mar 2025 18:26:11 +0100 Subject: [PATCH 11/11] add deeplink telemetry, show message when reusing connection with a different name --- src/connectionController.ts | 43 ++++++------- src/mdbExtensionController.ts | 11 +++- src/telemetry/telemetryEvents.ts | 29 ++++++++- src/test/suite/connectionController.test.ts | 13 +++- src/test/suite/mdbExtensionController.test.ts | 61 +++++++++++++++++++ src/views/statusView.ts | 9 +++ 6 files changed, 138 insertions(+), 28 deletions(-) diff --git a/src/connectionController.ts b/src/connectionController.ts index 37785ddde..06ef42e09 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -146,7 +146,6 @@ export default class ConnectionController { _connectionStorage: ConnectionStorage; _telemetryService: TelemetryService; - private readonly _serviceName = 'mdb.vscode.savedConnections'; private _currentConnectionId: null | string = null; _connectionAttempt: null | ConnectionAttempt = null; @@ -321,13 +320,20 @@ export default class ConnectionController { const connectionStringData = new ConnectionString(connectionString); try { - let existingId: string | undefined; + let existingConnection: LoadedConnection | undefined; if (reuseExisting) { - existingId = this._findConnectionIdByConnectionString(connectionString); + existingConnection = + this._findConnectionByConnectionString(connectionString); + + if (existingConnection && existingConnection.name !== name) { + void vscode.window.showInformationMessage( + `Connection with the same connection string already exists, under a different name: '${existingConnection.name}'. Connecting to the existing one...` + ); + } } - const connectResult = await (existingId - ? this.connectWithConnectionId(existingId) + const connectResult = await (existingConnection + ? this.connectWithConnectionId(existingConnection.id) : this.saveNewConnectionAndConnect({ connectionId: uuidv4(), connectionOptions: { @@ -536,13 +542,7 @@ export default class ConnectionController { log.info('Successfully connected', { connectionId }); - const message = 'MongoDB connection successful.'; - this._statusView.showMessage(message); - setTimeout(() => { - if (this._statusView._statusBarItem.text === message) { - this._statusView.hideMessage(); - } - }, 5000); + this._statusView.showTemporaryMessage('MongoDB connection successful.'); dataService.addReauthenticationHandler( this._reauthenticationHandler.bind(this) @@ -598,9 +598,9 @@ export default class ConnectionController { } } - _findConnectionIdByConnectionString( + _findConnectionByConnectionString( connectionString: string - ): string | undefined { + ): LoadedConnection | undefined { const searchStrings = [connectionString]; if (!connectionString.endsWith('/')) { searchStrings.push(`${connectionString}/`); @@ -608,7 +608,7 @@ export default class ConnectionController { return this.getConnectionsFromHistory().find((connection) => searchStrings.includes(connection.connectionOptions?.connectionString) - )?.id; + ); } private async onConnectSuccess({ @@ -758,14 +758,7 @@ export default class ConnectionController { this._disconnecting = false; - const message = 'MongoDB disconnected.'; - this._statusView.showMessage(message); - setTimeout(() => { - if (this._statusView._statusBarItem.text === message) { - this._statusView.hideMessage(); - } - }, 5000); - + this._statusView.showTemporaryMessage('MongoDB disconnected.'); return true; } @@ -845,9 +838,9 @@ export default class ConnectionController { if ('id' in options) { connectionIdToRemove = options.id; } else if ('connectionString' in options) { - const connectionId = this._findConnectionIdByConnectionString( + const connectionId = this._findConnectionByConnectionString( options.connectionString - ); + )?.id; if (!connectionId) { // No connection to remove, so just return silently. diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 5613a43d9..2a82a2efb 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -33,7 +33,7 @@ import launchMongoShell from './commands/launchMongoShell'; import type SchemaTreeItem from './explorer/schemaTreeItem'; import { StatusView } from './views'; import { StorageController, StorageVariables } from './storage'; -import { TelemetryService } from './telemetry'; +import { DeepLinkTelemetryEvent, TelemetryService } from './telemetry'; import type PlaygroundsTreeItem from './explorer/playgroundsTreeItem'; import PlaygroundResultProvider from './editors/playgroundResultProvider'; import WebviewController from './views/webviewController'; @@ -230,6 +230,14 @@ export default class MDBExtensionController implements vscode.Disposable { parseNumbers: true, }); + const source = + 'utm_source' in parameters && typeof parameters.utm_source === 'string' + ? parameters.utm_source + : undefined; + + delete parameters.utm_source; // Don't propagate after tracking. + this._telemetryService.track(new DeepLinkTelemetryEvent(command, source)); + try { if ( !Object.values(EXTENSION_COMMANDS).includes( @@ -240,6 +248,7 @@ export default class MDBExtensionController implements vscode.Disposable { `Unable to execute command '${command}' since it is not registered by the MongoDB extension.` ); } + await vscode.commands.executeCommand(command, parameters); } catch (error) { await vscode.window.showErrorMessage( diff --git a/src/telemetry/telemetryEvents.ts b/src/telemetry/telemetryEvents.ts index a454f02f8..a97476af2 100644 --- a/src/telemetry/telemetryEvents.ts +++ b/src/telemetry/telemetryEvents.ts @@ -669,6 +669,32 @@ export class TreeItemExpandedTelemetryEvent implements TelemetryEventBase { } } +/** + * Reported when the extension handles a deep link (e.g. vscode://mongodb.mongodb-vscode/command) + */ +export class DeepLinkTelemetryEvent implements TelemetryEventBase { + type = 'Deep Link Handled'; + properties: { + /** + * The command that the deeplink requested - e.g. `mdb.connectWithURI`. This event is reported even + * if the command is not valid and an error eventually shown to the user. + */ + command: string; + + /** + * The source of the deep link - e.g. the Atlas CLI or the docs website. + */ + source?: string; + }; + + constructor(command: string, source?: string) { + this.properties = { + command, + source, + }; + } +} + export type TelemetryEvent = | PlaygroundExecutedTelemetryEvent | LinkClickedTelemetryEvent @@ -693,4 +719,5 @@ export type TelemetryEvent = | ParticipantResponseGeneratedTelemetryEvent | PresetConnectionEditedTelemetryEvent | SidePanelOpenedTelemetryEvent - | TreeItemExpandedTelemetryEvent; + | TreeItemExpandedTelemetryEvent + | DeepLinkTelemetryEvent; diff --git a/src/test/suite/connectionController.test.ts b/src/test/suite/connectionController.test.ts index fd5f7acf8..37292e6f2 100644 --- a/src/test/suite/connectionController.test.ts +++ b/src/test/suite/connectionController.test.ts @@ -1624,6 +1624,7 @@ suite('Connection Controller Test Suite', function () { 2 ); expect(fakeConnect).to.have.been.calledTwice; + expect(showInformationMessageStub).to.not.have.been.called; }); test('true, reuses existing connection', async () => { @@ -1638,10 +1639,11 @@ suite('Connection Controller Test Suite', function () { ); expect(fakeConnect).to.have.been.calledOnce; + expect(showInformationMessageStub).to.not.have.been.called; // First time we're adding this connection await testConnectionController.addNewConnectionStringAndConnect({ connectionString: TEST_DATABASE_URI_USER, - name: 'bar', + name: 'foo', reuseExisting: true, }); @@ -1649,6 +1651,9 @@ suite('Connection Controller Test Suite', function () { 1 ); expect(fakeConnect).to.have.been.calledTwice; + + // Adding a connection with the same connection string and name should not show a message + expect(showInformationMessageStub).to.not.have.been.called; }); test('true, does not override existing connection name', async () => { @@ -1661,6 +1666,7 @@ suite('Connection Controller Test Suite', function () { let connections = testConnectionController.getSavedConnections(); expect(connections).to.have.lengthOf(1); expect(connections[0].name).to.equal('foo'); + expect(showInformationMessageStub).to.not.have.been.called; // First time we're adding this connection await testConnectionController.addNewConnectionStringAndConnect({ connectionString: TEST_DATABASE_URI_USER, @@ -1671,6 +1677,11 @@ suite('Connection Controller Test Suite', function () { connections = testConnectionController.getSavedConnections(); expect(connections).to.have.lengthOf(1); expect(connections[0].name).to.equal('foo'); // not 'bar' + + // Connecting with a different name should show a message + expect(showInformationMessageStub).to.have.been.calledOnceWith( + "Connection with the same connection string already exists, under a different name: 'foo'. Connecting to the existing one..." + ); }); test('true, matches connection regardless of trailing slash', async () => { diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index 1cb3e6c96..02a525ddd 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -29,6 +29,7 @@ import { import { VIEW_COLLECTION_SCHEME } from '../../editors/collectionDocumentsProvider'; import type { CollectionDetailsType } from '../../explorer/collectionTreeItem'; import { expect } from 'chai'; +import { DeepLinkTelemetryEvent } from '../../telemetry'; const testDatabaseURI = 'mongodb://localhost:27088'; @@ -1770,12 +1771,17 @@ suite('MDBExtensionController Test Suite', function () { suite('handleDeepLink', () => { let fakeExecuteCommand: sinon.SinonStub; + let fakeTrack: sinon.SinonStub; let fakeShowErrorMessage: sinon.SinonSpy; beforeEach(() => { fakeExecuteCommand = sandbox.stub(vscode.commands, 'executeCommand'); fakeShowErrorMessage = sandbox.stub(vscode.window, 'showErrorMessage'); + fakeTrack = sandbox.stub( + mdbTestExtension.testExtensionController._telemetryService, + 'track' + ); }); afterEach(() => { @@ -1876,5 +1882,60 @@ suite('MDBExtensionController Test Suite', function () { "Failed to handle 'vscode://mongodb.mongodb-vscode/mdb.connectWithURI': Error: fake error" ); }); + + test('reports telemetry event', async () => { + await mdbTestExtension.testExtensionController._handleDeepLink( + vscode.Uri.parse( + 'vscode://mongodb.mongodb-vscode/connectWithURI?foo=true&bar=987&baz=str' + ) + ); + + expect(fakeTrack).to.have.been.calledOnceWith( + new DeepLinkTelemetryEvent('mdb.connectWithURI') + ); + }); + + test('reports utm_source if present', async () => { + await mdbTestExtension.testExtensionController._handleDeepLink( + vscode.Uri.parse( + 'vscode://mongodb.mongodb-vscode/connectWithURI?foo=true&bar=987&baz=str&utm_source=AtlasCLI' + ) + ); + + expect(fakeTrack).to.have.been.calledOnceWith( + new DeepLinkTelemetryEvent('mdb.connectWithURI', 'AtlasCLI') + ); + }); + + test('reports even non-existent commands', async () => { + await mdbTestExtension.testExtensionController._handleDeepLink( + vscode.Uri.parse( + 'vscode://mongodb.mongodb-vscode/invalid_command?foo=true&bar=987&baz=str&utm_source=RogueActor' + ) + ); + + expect(fakeTrack).to.have.been.calledOnceWith( + new DeepLinkTelemetryEvent('mdb.invalid_command', 'RogueActor') + ); + }); + + test('removes utm_source from parameters passed to command', async () => { + await mdbTestExtension.testExtensionController._handleDeepLink( + vscode.Uri.parse( + 'vscode://mongodb.mongodb-vscode/mdb.connectWithURI?foo=bar&utm_source=abc' + ) + ); + + expect(fakeExecuteCommand).to.have.been.calledWith('mdb.connectWithURI', { + foo: 'bar', + }); + expect(fakeExecuteCommand.firstCall.args[1]).to.not.have.property( + 'utm_source' + ); + expect(fakeTrack).to.have.been.calledOnceWith( + new DeepLinkTelemetryEvent('mdb.connectWithURI', 'abc') + ); + expect(fakeShowErrorMessage).to.not.have.been.called; + }); }); }); diff --git a/src/views/statusView.ts b/src/views/statusView.ts index b367f2143..181f7ca36 100644 --- a/src/views/statusView.ts +++ b/src/views/statusView.ts @@ -17,6 +17,15 @@ export default class StatusView { this._statusBarItem.show(); } + public showTemporaryMessage(message: string): void { + this.showMessage(message); + setTimeout(() => { + if (this._statusBarItem.text === message) { + this.hideMessage(); + } + }, 5000); + } + public hideMessage(): void { this._statusBarItem.hide(); }