diff --git a/.vscode/settings.json b/.vscode/settings.json index 32baf6163..8dc745b7f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,5 +16,11 @@ "out": true }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" + "typescript.tsc.autoDetect": "off", + "mdb.presetConnections": [ + { + "name": "Preset Connection", + "connectionString": "mongodb://localhost:27017" + } + ] } diff --git a/package.json b/package.json index 4e0fac3db..534a89314 100644 --- a/package.json +++ b/package.json @@ -320,6 +320,10 @@ "command": "mdb.copyConnectionString", "title": "Copy Connection String" }, + { + "command": "mdb.editPresetConnections", + "title": "Edit Preset Connections..." + }, { "command": "mdb.renameConnection", "title": "Rename Connection..." @@ -489,42 +493,49 @@ }, { "command": "mdb.addConnection", - "when": "view == mongoDBConnectionExplorer" + "when": "view == mongoDBConnectionExplorer", + "group": "1@1" }, { "command": "mdb.addConnectionWithURI", - "when": "view == mongoDBConnectionExplorer" + "when": "view == mongoDBConnectionExplorer", + "group": "1@2" + }, + { + "command": "mdb.editPresetConnections", + "when": "view == mongoDBConnectionExplorer", + "group": "2@1" } ], "view/item/context": [ { "command": "mdb.addDatabase", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem && mdb.isAtlasStreams == false", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem) && mdb.isAtlasStreams == false", "group": "inline" }, { "command": "mdb.addDatabase", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem && mdb.isAtlasStreams == false", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem) && mdb.isAtlasStreams == false", "group": "1@1" }, { "command": "mdb.addStreamProcessor", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem && mdb.isAtlasStreams == true", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem) && mdb.isAtlasStreams == true", "group": "inline" }, { "command": "mdb.addStreamProcessor", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem && mdb.isAtlasStreams == true", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem) && mdb.isAtlasStreams == true", "group": "1@1" }, { "command": "mdb.refreshConnection", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem)", "group": "1@2" }, { "command": "mdb.treeViewOpenMongoDBShell", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem)", "group": "2@1" }, { @@ -537,14 +548,19 @@ "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", "group": "3@2" }, + { + "command": "mdb.editPresetConnections", + "when": "view == mongoDBConnectionExplorer && viewItem == connectedPresetConnectionTreeItem", + "group": "3@2" + }, { "command": "mdb.copyConnectionString", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem)", "group": "4@1" }, { "command": "mdb.disconnectFromConnectionTreeItem", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem)", "group": "5@1" }, { @@ -559,7 +575,7 @@ }, { "command": "mdb.connectToConnectionTreeItem", - "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == disconnectedConnectionTreeItem || viewItem == disconnectedPresetConnectionTreeItem)", "group": "1@1" }, { @@ -572,9 +588,14 @@ "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedConnectionTreeItem", "group": "2@2" }, + { + "command": "mdb.editPresetConnections", + "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedPresetConnectionTreeItem", + "group": "2@2" + }, { "command": "mdb.copyConnectionString", - "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == disconnectedConnectionTreeItem || viewItem == disconnectedPresetConnectionTreeItem)", "group": "3@1" }, { @@ -1171,6 +1192,42 @@ "type": "string", "default": "", "description": "Specify a shell command that is run to start the browser for authenticating with the OIDC identity provider for the server connection. Leave this empty for default browser." + }, + "mdb.presetConnections": { + "scope": "window", + "type": "array", + "description": "Defines preset connections. Can be used to share connection configurations in a workspace or global scope. Do not store sensitive credentials here.", + "examples": [ + [ + { + "name": "Preset Connection", + "connectionString": "mongodb://localhost:27017" + } + ] + ], + "items": { + "type": "object", + "examples": [ + { + "name": "Preset Connection", + "connectionString": "mongodb://localhost:27017" + } + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the connection." + }, + "connectionString": { + "type": "string", + "description": "Connection string. Do not store sensitive credentials here." + } + }, + "required": [ + "name", + "connectionString" + ] + } } } }, diff --git a/src/commands/index.ts b/src/commands/index.ts index b1a23606c..348189649 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -44,6 +44,7 @@ enum EXTENSION_COMMANDS { MDB_EDIT_CONNECTION = 'mdb.editConnection', MDB_REFRESH_CONNECTION = 'mdb.refreshConnection', MDB_COPY_CONNECTION_STRING = 'mdb.copyConnectionString', + MDB_EDIT_PRESET_CONNECTIONS = 'mdb.editPresetConnections', MDB_REMOVE_CONNECTION_TREE_VIEW = 'mdb.treeItemRemoveConnection', MDB_RENAME_CONNECTION = 'mdb.renameConnection', MDB_ADD_DATABASE = 'mdb.addDatabase', diff --git a/src/connectionController.ts b/src/connectionController.ts index 4bffaa094..159ecea8e 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -21,10 +21,15 @@ import type { StorageController } from './storage'; import type { StatusView } from './views'; import type TelemetryService from './telemetry/telemetryService'; import { openLink } from './utils/linkHelper'; -import type { LoadedConnection } from './storage/connectionStorage'; +import type { + ConnectionSource, + LoadedConnection, +} from './storage/connectionStorage'; import { ConnectionStorage } from './storage/connectionStorage'; import LINKS from './utils/links'; import { isAtlasStream } from 'mongodb-build-info'; +import { DocumentSource } from './documentSource'; +import type { ConnectionTreeItem } from './explorer'; // eslint-disable-next-line @typescript-eslint/no-var-requires const packageJSON = require('../package.json'); @@ -161,7 +166,55 @@ export default class ConnectionController { }); } + async openPresetConnectionsSettings( + originTreeItem: ConnectionTreeItem | undefined + ): Promise { + this._telemetryService.trackPresetConnectionEdited({ + source: DocumentSource.DOCUMENT_SOURCE_TREEVIEW, + source_details: originTreeItem ? 'tree_item' : 'header', + }); + let source: ConnectionSource | undefined = originTreeItem?.source; + if (!source) { + const mdbConfiguration = vscode.workspace.getConfiguration('mdb'); + + const presetConnections = mdbConfiguration?.inspect('presetConnections'); + + if (presetConnections?.workspaceValue) { + source = 'workspaceSettings'; + } else if (presetConnections?.globalValue) { + source = 'globalSettings'; + } else { + // If no preset settings exist in workspace and global scope, + // set a default one inside the workspace and open it. + source = 'workspaceSettings'; + await mdbConfiguration.update('presetConnections', [ + { + name: 'Preset Connection', + connectionString: 'mongodb://localhost:27017', + }, + ]); + } + } + switch (source) { + case 'globalSettings': + await vscode.commands.executeCommand( + 'workbench.action.openSettingsJson' + ); + break; + case 'workspaceSettings': + case 'user': + await vscode.commands.executeCommand( + 'workbench.action.openWorkspaceSettingsFile' + ); + break; + default: + throw new Error('Unknown preset connection source'); + } + } + async loadSavedConnections(): Promise { + this._connections = Object.create(null); + const loadedConnections = await this._connectionStorage.loadConnections(); for (const connection of loadedConnections) { diff --git a/src/explorer/connectionTreeItem.ts b/src/explorer/connectionTreeItem.ts index 9c47423a4..53f83da70 100644 --- a/src/explorer/connectionTreeItem.ts +++ b/src/explorer/connectionTreeItem.ts @@ -11,11 +11,11 @@ import formatError from '../utils/formatError'; import { getImagesPath } from '../extensionConstants'; import type TreeItemParent from './treeItemParentInterface'; import StreamProcessorTreeItem from './streamProcessorTreeItem'; +import type { ConnectionSource } from '../storage/connectionStorage'; -export enum ConnectionItemContextValues { - disconnected = 'disconnectedConnectionTreeItem', - connected = 'connectedConnectionTreeItem', -} +export type ConnectionItemContextValue = `${'disconnected' | 'connected'}${ + | '' + | 'Preset'}ConnectionTreeItem`; function getIconPath(isActiveConnection: boolean): { light: string; @@ -39,7 +39,7 @@ export default class ConnectionTreeItem extends vscode.TreeItem implements TreeItemParent, vscode.TreeDataProvider { - contextValue = ConnectionItemContextValues.disconnected; + contextValue: ConnectionItemContextValue = 'disconnectedConnectionTreeItem'; private _childrenCache: { [key: string]: DatabaseTreeItem | StreamProcessorTreeItem; @@ -50,6 +50,7 @@ export default class ConnectionTreeItem connectionId: string; isExpanded: boolean; + source: ConnectionSource; constructor({ connectionId, @@ -58,6 +59,7 @@ export default class ConnectionTreeItem connectionController, cacheIsUpToDate, childrenCache, + source, }: { connectionId: string; collapsibleState: vscode.TreeItemCollapsibleState; @@ -67,21 +69,24 @@ export default class ConnectionTreeItem childrenCache: { [key: string]: DatabaseTreeItem | StreamProcessorTreeItem; }; // Existing cache. + source: ConnectionSource; }) { super( connectionController.getSavedConnectionName(connectionId), collapsibleState ); - if ( + const isConnected = connectionController.getActiveConnectionId() === connectionId && !connectionController.isDisconnecting() && - !connectionController.isConnecting() - ) { - this.contextValue = ConnectionItemContextValues.connected; - } + !connectionController.isConnecting(); + + this.contextValue = `${isConnected ? 'connected' : 'disconnected'}${ + source === 'user' ? '' : 'Preset' + }ConnectionTreeItem`; this.connectionId = connectionId; + this.source = source; this._connectionController = connectionController; this.isExpanded = isExpanded; this._childrenCache = childrenCache; @@ -204,7 +209,9 @@ export default class ConnectionTreeItem return Object.values(this._childrenCache); } - private async _buildChildrenCacheForDatabases(dataService: DataService) { + private async _buildChildrenCacheForDatabases( + dataService: DataService + ): Promise> { const databases = await this.listDatabases(); databases.sort((a: string, b: string) => a.localeCompare(b)); @@ -226,7 +233,9 @@ export default class ConnectionTreeItem return newChildrenCache; } - private async _buildChildrenCacheForStreams(dataService: DataService) { + private async _buildChildrenCacheForStreams( + dataService: DataService + ): Promise> { const processors = await this.listStreamProcessors(); processors.sort((a, b) => a.name.localeCompare(b.name)); diff --git a/src/explorer/explorerTreeController.ts b/src/explorer/explorerTreeController.ts index 7c569cfb2..851fc3ac3 100644 --- a/src/explorer/explorerTreeController.ts +++ b/src/explorer/explorerTreeController.ts @@ -8,6 +8,7 @@ import { DOCUMENT_ITEM } from './documentTreeItem'; import { DOCUMENT_LIST_ITEM, CollectionTypes } from './documentListTreeItem'; import EXTENSION_COMMANDS from '../commands'; import { sortTreeItemsByLabel } from './treeItemUtils'; +import type { LoadedConnection } from '../storage/connectionStorage'; const log = createLogger('explorer tree controller'); @@ -130,6 +131,50 @@ export default class ExplorerTreeController return element; } + private _getConnectionExpandedState( + connection: LoadedConnection, + pastConnectionTreeItems: { + [key: string]: ConnectionTreeItem; + } + ): { + collapsibleState: vscode.TreeItemCollapsibleState; + isExpanded: boolean; + } { + const isActiveConnection = + connection.id === this._connectionController.getActiveConnectionId(); + const isBeingConnectedTo = + this._connectionController.isConnecting() && + connection.id === this._connectionController.getConnectingConnectionId(); + + let collapsibleState = isActiveConnection + ? vscode.TreeItemCollapsibleState.Expanded + : vscode.TreeItemCollapsibleState.Collapsed; + + if ( + pastConnectionTreeItems[connection.id] && + !pastConnectionTreeItems[connection.id].isExpanded + ) { + // Connection was manually collapsed while being active. + collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; + } + if (isActiveConnection && this._connectionController.isDisconnecting()) { + // Don't show a collapsable state when the connection is being disconnected from. + collapsibleState = vscode.TreeItemCollapsibleState.None; + } + if (isBeingConnectedTo) { + // Don't show a collapsable state when the connection is being connected to. + collapsibleState = vscode.TreeItemCollapsibleState.None; + } + return { + collapsibleState, + // Set expanded when we're connecting to a connection so that it + // expands when it's connected. + isExpanded: + isBeingConnectedTo || + collapsibleState === vscode.TreeItemCollapsibleState.Expanded, + }; + } + getChildren(element?: any): Thenable { // When no element is present we are at the root. if (!element) { @@ -139,45 +184,14 @@ export default class ExplorerTreeController // Create new connection tree items, using cached children wherever possible. connections.forEach((connection) => { - const isActiveConnection = - connection.id === this._connectionController.getActiveConnectionId(); - const isBeingConnectedTo = - this._connectionController.isConnecting() && - connection.id === - this._connectionController.getConnectingConnectionId(); - - let connectionExpandedState = isActiveConnection - ? vscode.TreeItemCollapsibleState.Expanded - : vscode.TreeItemCollapsibleState.Collapsed; - - if ( - pastConnectionTreeItems[connection.id] && - !pastConnectionTreeItems[connection.id].isExpanded - ) { - // Connection was manually collapsed while being active. - connectionExpandedState = vscode.TreeItemCollapsibleState.Collapsed; - } - if ( - isActiveConnection && - this._connectionController.isDisconnecting() - ) { - // Don't show a collapsable state when the connection is being disconnected from. - connectionExpandedState = vscode.TreeItemCollapsibleState.None; - } - if (isBeingConnectedTo) { - // Don't show a collapsable state when the connection is being connected to. - connectionExpandedState = vscode.TreeItemCollapsibleState.None; - } + const { collapsibleState, isExpanded } = + this._getConnectionExpandedState(connection, pastConnectionTreeItems); this._connectionTreeItems[connection.id] = new ConnectionTreeItem({ connectionId: connection.id, - collapsibleState: connectionExpandedState, - // Set expanded when we're connecting to a connection so that it - // expands when it's connected. - isExpanded: - isBeingConnectedTo || - connectionExpandedState === - vscode.TreeItemCollapsibleState.Expanded, + collapsibleState, + isExpanded, + source: connection.source ?? 'user', connectionController: this._connectionController, cacheIsUpToDate: pastConnectionTreeItems[connection.id] ? pastConnectionTreeItems[connection.id].cacheIsUpToDate diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index b9a2de1c3..b6f0b9505 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -160,6 +160,15 @@ export default class MDBExtensionController implements vscode.Disposable { this._editorsController.registerProviders(); } + subscribeToConfigurationChanges(): void { + const subscription = vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration('mdb.presetConnections')) { + void this._connectionController.loadSavedConnections(); + } + }); + this._context.subscriptions.push(subscription); + } + async activate(): Promise { this._explorerController.activateConnectionsTreeView(); this._helpExplorer.activateHelpTreeView(this._telemetryService); @@ -172,6 +181,7 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerCommands(); this.showOverviewPageIfRecentlyInstalled(); + this.subscribeToConfigurationChanges(); const copilot = vscode.extensions.getExtension(COPILOT_EXTENSION_ID); void vscode.commands.executeCommand( @@ -480,6 +490,13 @@ export default class MDBExtensionController implements vscode.Disposable { return true; } ); + this.registerCommand( + EXTENSION_COMMANDS.MDB_EDIT_PRESET_CONNECTIONS, + async (element: ConnectionTreeItem | undefined) => { + await this._connectionController.openPresetConnectionsSettings(element); + return true; + } + ); this.registerCommand( EXTENSION_COMMANDS.MDB_COPY_CONNECTION_STRING, async (element: ConnectionTreeItem): Promise => { diff --git a/src/storage/connectionStorage.ts b/src/storage/connectionStorage.ts index e233ad4d5..853ef12e2 100644 --- a/src/storage/connectionStorage.ts +++ b/src/storage/connectionStorage.ts @@ -11,21 +11,34 @@ import type StorageController from './storageController'; import type { SecretStorageLocationType } from './storageController'; import { DefaultSavingLocations, + SecretStorageLocation, StorageLocation, StorageVariables, } from './storageController'; +import { v4 as uuidv4 } from 'uuid'; const log = createLogger('connection storage'); +export type ConnectionSource = 'globalSettings' | 'workspaceSettings' | 'user'; export interface StoreConnectionInfo { id: string; // Connection model id or a new uuid. name: string; // Possibly user given name, not unique. storageLocation: StorageLocation; secretStorageLocation?: SecretStorageLocationType; connectionOptions?: ConnectionOptions; + source?: ConnectionSource; lastUsed?: Date; // Date and time when the connection was last used, i.e. connected with. } +export type PresetSavedConnection = { + name: string; + connectionString: string; +}; + +export type PresetSavedConnectionWithSource = PresetSavedConnection & { + source: ConnectionSource; +}; + type StoreConnectionInfoWithConnectionOptions = StoreConnectionInfo & Required>; @@ -56,6 +69,7 @@ export class ConnectionStorage { return { id: connectionId, name, + source: 'user', storageLocation: this.getPreferredStorageLocationFromConfiguration(), secretStorageLocation: 'vscode.SecretStorage', connectionOptions: connectionOptions, @@ -166,7 +180,42 @@ export class ConnectionStorage { ); } - async loadConnections() { + _loadPresetConnections(): LoadedConnection[] { + const configuration = vscode.workspace.getConfiguration('mdb'); + const presetConnectionsInfo = + configuration.inspect('presetConnections'); + + if (!presetConnectionsInfo) { + return []; + } + + const combinedPresetConnections: PresetSavedConnectionWithSource[] = [ + ...(presetConnectionsInfo?.globalValue ?? []).map((preset) => ({ + ...preset, + source: 'globalSettings' as const, + })), + ...(presetConnectionsInfo?.workspaceValue ?? []).map((preset) => ({ + ...preset, + source: 'workspaceSettings' as const, + })), + ]; + + return combinedPresetConnections.map( + (presetConnection) => + ({ + id: uuidv4(), + name: presetConnection.name, + connectionOptions: { + connectionString: presetConnection.connectionString, + }, + source: presetConnection.source, + storageLocation: StorageLocation.NONE, + secretStorageLocation: SecretStorageLocation.SecretStorage, + } satisfies LoadedConnection) + ); + } + + async loadConnections(): Promise { const globalAndWorkspaceConnections = Object.values({ ...this._storageController.get( StorageVariables.GLOBAL_SAVED_CONNECTIONS, @@ -203,10 +252,12 @@ export class ConnectionStorage { }) ); - return loadedConnections; + const presetConnections = this._loadPresetConnections(); + + return [...loadedConnections, ...presetConnections]; } - async removeConnection(connectionId: string) { + async removeConnection(connectionId: string): Promise { await this._storageController.deleteSecret(connectionId); // See if the connection exists in the saved global or workspace connections diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index 232031994..82ccd523b 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -91,6 +91,8 @@ type ConnectionEditedTelemetryEventProperties = { type SavedConnectionsLoadedProperties = { // Total number of connections saved on disk saved_connections: number; + // Total number of connections from preset settings + preset_connections: number; // Total number of connections that extension was able to load, it might // differ from saved_connections since there might be failures in loading // secrets for a connection in which case we don't list the connections in the @@ -145,6 +147,11 @@ export type ParticipantChatOpenedFromActionProperties = { command?: ParticipantCommandType; }; +export type PresetSavedConnectionEditedProperties = { + source: DocumentSource; + source_details: 'tree_item' | 'header'; +}; + export type ParticipantInputBoxSubmitted = { source: DocumentSource; input_length: number | undefined; @@ -216,6 +223,7 @@ export enum TelemetryEventTypes { /** Tracks after a participant interacts with the input box we open to let the user write the prompt for participant. */ PARTICIPANT_INPUT_BOX_SUBMITTED = 'Participant Inbox Box Submitted', PARTICIPANT_RESPONSE_GENERATED = 'Participant Response Generated', + PRESET_CONNECTION_EDITED = 'Preset Connection Edited', } /** @@ -446,6 +454,12 @@ export default class TelemetryService { ); } + trackPresetConnectionEdited( + props: PresetSavedConnectionEditedProperties + ): void { + this.track(TelemetryEventTypes.PRESET_CONNECTION_EDITED, props); + } + trackPlaygroundCreated(playgroundType: string): void { this.track(TelemetryEventTypes.PLAYGROUND_CREATED, { playground_type: playgroundType, diff --git a/src/test/suite/connectionController.test.ts b/src/test/suite/connectionController.test.ts index 78f7d649d..0f54d5820 100644 --- a/src/test/suite/connectionController.test.ts +++ b/src/test/suite/connectionController.test.ts @@ -240,6 +240,31 @@ suite('Connection Controller Test Suite', function () { expect(testConnectionController.getSavedConnections().length).to.equal(0); }); + test('clears connections when loading saved connections', async () => { + // This might happen if i.e. one defines a preset connection and then deletes it. + // In that case we'd have defined this connection but there was never a follow up + // delete event to clear it. So on reload we need to start from a clean slate. + testConnectionController._connections['1234'] = { + id: '1234', + name: 'orphan', + connectionOptions: { + connectionString: 'localhost:3000', + }, + storageLocation: StorageLocation.NONE, + secretStorageLocation: SecretStorageLocation.SecretStorage, + }; + + // Should persist as this is a saved connection. + await testConnectionController.addNewConnectionStringAndConnect( + TEST_DATABASE_URI + ); + + await testConnectionController.loadSavedConnections(); + + expect(testConnectionController.getSavedConnections().length).to.equal(1); + expect(testConnectionController._connections['1234']).is.undefined; + }); + test('the connection model loads both global and workspace stored connection models', async () => { const expectedDriverUrl = `mongodb://localhost:27088/?appname=mongodb-vscode+${version}`; diff --git a/src/test/suite/explorer/connectionTreeItem.test.ts b/src/test/suite/explorer/connectionTreeItem.test.ts index 13f7bd157..4e65a0042 100644 --- a/src/test/suite/explorer/connectionTreeItem.test.ts +++ b/src/test/suite/explorer/connectionTreeItem.test.ts @@ -4,9 +4,7 @@ import { beforeEach, afterEach } from 'mocha'; import sinon from 'sinon'; import type { DataService } from 'mongodb-data-service'; -import ConnectionTreeItem, { - ConnectionItemContextValues, -} from '../../../explorer/connectionTreeItem'; +import ConnectionTreeItem from '../../../explorer/connectionTreeItem'; import { DataServiceStub } from '../stubs'; import formatError from '../../../utils/formatError'; import { mdbTestExtension } from '../stubbableMdbExtension'; @@ -14,7 +12,7 @@ import { mdbTestExtension } from '../stubbableMdbExtension'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { contributes } = require('../../../../package.json'); -function getTestConnectionTreeItem() { +function getTestConnectionTreeItem(): ConnectionTreeItem { return new ConnectionTreeItem({ connectionId: 'test', collapsibleState: vscode.TreeItemCollapsibleState.Expanded, @@ -23,6 +21,7 @@ function getTestConnectionTreeItem() { mdbTestExtension.testExtensionController._connectionController, cacheIsUpToDate: false, childrenCache: {}, + source: 'user', }); } @@ -32,10 +31,10 @@ suite('ConnectionTreeItem Test Suite', () => { let disconnectedRegisteredCommandInPackageJson = false; contributes.menus['view/item/context'].forEach((contextItem) => { - if (contextItem.when.includes(ConnectionItemContextValues.connected)) { + if (contextItem.when.includes('connected')) { connectedRegisteredCommandInPackageJson = true; } - if (contextItem.when.includes(ConnectionItemContextValues.disconnected)) { + if (contextItem.when.includes('disconnected')) { disconnectedRegisteredCommandInPackageJson = true; } }); diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index 440a83262..518b917bc 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -42,6 +42,7 @@ function getTestConnectionTreeItem( mdbTestExtension.testExtensionController._connectionController, cacheIsUpToDate: false, childrenCache: {}, + source: 'user', ...options, }); } diff --git a/src/test/suite/storage/connectionStorage.test.ts b/src/test/suite/storage/connectionStorage.test.ts index 30ae15719..2f336547d 100644 --- a/src/test/suite/storage/connectionStorage.test.ts +++ b/src/test/suite/storage/connectionStorage.test.ts @@ -15,12 +15,15 @@ import { TEST_DATABASE_URI_USER, TEST_USER_PASSWORD, } from '../dbTestHelper'; -import type { StoreConnectionInfo } from '../../../storage/connectionStorage'; +import type { LoadedConnection } from '../../../storage/connectionStorage'; import { ConnectionStorage } from '../../../storage/connectionStorage'; const testDatabaseConnectionName = 'localhost:27088'; -const newTestConnection = (connectionStorage: ConnectionStorage, id: string) => +const newTestConnection = ( + connectionStorage: ConnectionStorage, + id: string +): LoadedConnection => connectionStorage.createNewConnection({ connectionId: id, connectionOptions: { @@ -305,9 +308,7 @@ suite('Connection Storage Test Suite', function () { expect(connections.length).to.equal(1); const newSavedConnectionInfoWithSecrets = - await testConnectionStorage._getConnectionInfoWithSecrets( - connections[0] as StoreConnectionInfo - ); + await testConnectionStorage._getConnectionInfoWithSecrets(connections[0]); expect(newSavedConnectionInfoWithSecrets).to.deep.equal(connectionInfo); }); @@ -321,6 +322,134 @@ suite('Connection Storage Test Suite', function () { extensionSandbox.restore(); }); + suite('when there are preset connections', () => { + const presetConnections = { + globalValue: [ + { + name: 'Global Connection 1', + connectionString: + 'mongodb://localhost:27017/?readPreference=primary&ssl=false', + }, + ], + workspaceValue: [ + { + name: 'Preset Connection 1', + connectionString: 'mongodb://localhost:27017', + }, + { + name: 'Preset Connection 2', + connectionString: 'mongodb://localhost:27018', + }, + ], + }; + + let getConfigurationStub: sinon.SinonStub< + [ + section?: string | undefined, + scope?: vscode.ConfigurationScope | null | undefined + ], + vscode.WorkspaceConfiguration + >; + let inspectPresetConnectionsStub: sinon.SinonStub; + + beforeEach(() => { + testSandbox.restore(); + inspectPresetConnectionsStub = testSandbox.stub(); + }); + + test('loads the preset connections', async () => { + getConfigurationStub = testSandbox.stub( + vscode.workspace, + 'getConfiguration' + ); + getConfigurationStub.returns({ + inspect: inspectPresetConnectionsStub, + get: () => undefined, + } as any); + + inspectPresetConnectionsStub + .withArgs('presetConnections') + .returns(presetConnections); + + const loadedConnections = await testConnectionStorage.loadConnections(); + + const expectedConnectionValues = [ + ...presetConnections.globalValue.map((connection) => ({ + ...connection, + source: 'globalSettings', + })), + ...presetConnections.workspaceValue.map((connection) => ({ + ...connection, + source: 'workspaceSettings', + })), + ]; + + expect(loadedConnections.length).equals( + expectedConnectionValues.length + ); + + for (let i = 0; i < expectedConnectionValues.length; i++) { + const connection = loadedConnections[i]; + const expected = expectedConnectionValues[i]; + expect(connection.name).equals(expected.name); + expect(connection.connectionOptions.connectionString).equals( + expected.connectionString + ); + expect(connection.source).equals(expected.source); + } + }); + + test('loads both preset and other saved connections', async () => { + const savedConnection = newTestConnection(testConnectionStorage, '1'); + await testConnectionStorage.saveConnection(savedConnection); + + getConfigurationStub = testSandbox.stub( + vscode.workspace, + 'getConfiguration' + ); + getConfigurationStub.returns({ + inspect: inspectPresetConnectionsStub, + get: () => undefined, + } as any); + + inspectPresetConnectionsStub + .withArgs('presetConnections') + .returns(presetConnections); + + const loadedConnections = await testConnectionStorage.loadConnections(); + + const expectedConnectionValues = [ + { + name: savedConnection.name, + source: 'user', + connectionString: `${savedConnection.connectionOptions.connectionString}/`, + }, + ...presetConnections.globalValue.map((connection) => ({ + ...connection, + source: 'globalSettings', + })), + ...presetConnections.workspaceValue.map((connection) => ({ + ...connection, + source: 'workspaceSettings', + })), + ]; + + expect(loadedConnections.length).equals( + expectedConnectionValues.length + ); + + for (let i = 0; i < expectedConnectionValues.length; i++) { + const connection = loadedConnections[i]; + const expected = expectedConnectionValues[i]; + expect(connection.name).equals(expected.name); + expect(connection.connectionOptions.connectionString).equals( + expected.connectionString + ); + expect(connection.source).equals(expected.source); + } + }); + }); + suite('when connection secrets are already in SecretStorage', () => { afterEach(() => { testSandbox.restore(); @@ -341,6 +470,8 @@ suite('Connection Storage Test Suite', function () { // By default the connection secrets are already stored in SecretStorage const savedConnections = await testConnectionStorage.loadConnections(); + + expect(savedConnections.length).equals(2); expect( savedConnections.every( ({ secretStorageLocation }) => diff --git a/src/test/suite/telemetry/telemetryService.test.ts b/src/test/suite/telemetry/telemetryService.test.ts index cfdcf8be1..f9830ca9e 100644 --- a/src/test/suite/telemetry/telemetryService.test.ts +++ b/src/test/suite/telemetry/telemetryService.test.ts @@ -672,6 +672,7 @@ suite('Telemetry Controller Test Suite', () => { testTelemetryService.trackSavedConnectionsLoaded({ saved_connections: 3, loaded_connections: 3, + preset_connections: 3, connections_with_secrets_in_keytar: 0, connections_with_secrets_in_secret_storage: 3, }); @@ -684,6 +685,7 @@ suite('Telemetry Controller Test Suite', () => { properties: { saved_connections: 3, loaded_connections: 3, + preset_connections: 3, connections_with_secrets_in_keytar: 0, connections_with_secrets_in_secret_storage: 3, },