Most Recently Used support in VSCode extension (#77)
* MRU support in ConnectionStore - Core functionality including tests to support most recently used list - Configuration option to let users define the size of the MRU list * Recent Connection added on Connect - ConnectionManager.Connect will save a connection to the recently used list - Unit Tests added to cover this - Default value from the user settings is now filtered out as this change caused it to be shown (and we do not want the sample value visible)
This commit is contained in:
Родитель
4e78839713
Коммит
ee9a4933cb
|
@ -167,6 +167,11 @@
|
|||
"default": false,
|
||||
"description": "[Optional] Log debug output to the VS Code console (Help -> Toggle Developer Tools)"
|
||||
},
|
||||
"vscode-mssql.maxRecentConnections": {
|
||||
"type": "number",
|
||||
"default": 5,
|
||||
"description": "The maximum number of recently used connections to store in the connection list"
|
||||
},
|
||||
"vscode-mssql.connections": {
|
||||
"type": "array",
|
||||
"default": [
|
||||
|
|
|
@ -40,7 +40,8 @@ export default class ConnectionManager {
|
|||
statusView: StatusView,
|
||||
prompter: IPrompter,
|
||||
private _client?: SqlToolsServerClient,
|
||||
private _vscodeWrapper?: VscodeWrapper) {
|
||||
private _vscodeWrapper?: VscodeWrapper,
|
||||
private _connectionStore?: ConnectionStore) {
|
||||
this._context = context;
|
||||
this._statusView = statusView;
|
||||
this._prompter = prompter;
|
||||
|
@ -53,7 +54,11 @@ export default class ConnectionManager {
|
|||
this.vscodeWrapper = new VscodeWrapper();
|
||||
}
|
||||
|
||||
this._connectionUI = new ConnectionUI(new ConnectionStore(context), prompter, this.vscodeWrapper);
|
||||
if (!this._connectionStore) {
|
||||
this._connectionStore = new ConnectionStore(context);
|
||||
}
|
||||
|
||||
this._connectionUI = new ConnectionUI(this._connectionStore, prompter, this.vscodeWrapper);
|
||||
|
||||
this.vscodeWrapper.onDidCloseTextDocument(params => this.onDidCloseTextDocument(params));
|
||||
this.vscodeWrapper.onDidSaveTextDocument(params => this.onDidSaveTextDocument(params));
|
||||
|
@ -287,16 +292,28 @@ export default class ConnectionManager {
|
|||
extensionConnectionTime: extensionTimer.getDuration() - serviceTimer.getDuration(),
|
||||
serviceConnectionTime: serviceTimer.getDuration()
|
||||
});
|
||||
|
||||
resolve(true);
|
||||
return newCredentials;
|
||||
} else {
|
||||
Utils.showErrorMsg(Constants.msgError + Constants.msgConnectionError);
|
||||
self.statusView.connectError(fileUri, connectionCreds, result.messages);
|
||||
self.connectionUI.showConnectionErrors(result.messages);
|
||||
|
||||
// We've logged the failure so no need to throw
|
||||
return undefined;
|
||||
}
|
||||
}).then( (newConnection: Interfaces.IConnectionCredentials) => {
|
||||
if (newConnection) {
|
||||
let connectionToSave: Interfaces.IConnectionCredentials = Object.assign({}, newConnection);
|
||||
self._connectionStore.addRecentlyUsed(connectionToSave)
|
||||
.then(() => {
|
||||
resolve(true);
|
||||
}, err => {
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
}, err => {
|
||||
// Catch unexpected errors and return over the Promise reject callback
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ export default class VscodeWrapper {
|
|||
* @param extensionName The string name of the extension to get the configuration for
|
||||
*/
|
||||
public getConfiguration(extensionName: string): vscode.WorkspaceConfiguration {
|
||||
return undefined;
|
||||
return vscode.workspace.getConfiguration(extensionName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,6 +4,7 @@ import Constants = require('./constants');
|
|||
import ConnInfo = require('./connectionInfo');
|
||||
import Utils = require('../models/utils');
|
||||
import ValidationException from '../utils/validationException';
|
||||
import VscodeWrapper from '../controllers/vscodeWrapper';
|
||||
import { ConnectionCredentials } from '../models/connectionCredentials';
|
||||
import { IConnectionCredentials, IConnectionProfile, IConnectionCredentialsQuickPickItem, CredentialsQuickPickItemType } from '../models/interfaces';
|
||||
import { ICredentialStore } from '../credentialstore/icredentialstore';
|
||||
|
@ -17,16 +18,17 @@ import { CredentialStore } from '../credentialstore/credentialstore';
|
|||
*/
|
||||
export class ConnectionStore {
|
||||
|
||||
private _context: vscode.ExtensionContext;
|
||||
private _credentialStore: ICredentialStore;
|
||||
constructor(
|
||||
private _context: vscode.ExtensionContext,
|
||||
private _credentialStore?: ICredentialStore,
|
||||
private _vscodeWrapper?: VscodeWrapper) {
|
||||
|
||||
constructor(context: vscode.ExtensionContext, credentialStore?: ICredentialStore) {
|
||||
this._context = context;
|
||||
if (credentialStore) {
|
||||
this._credentialStore = credentialStore;
|
||||
} else {
|
||||
if (!this._credentialStore) {
|
||||
this._credentialStore = new CredentialStore();
|
||||
}
|
||||
if (!this._vscodeWrapper) {
|
||||
this._vscodeWrapper = new VscodeWrapper();
|
||||
}
|
||||
}
|
||||
|
||||
public static get CRED_PREFIX(): string { return 'Microsoft.SqlTools'; }
|
||||
|
@ -87,34 +89,24 @@ export class ConnectionStore {
|
|||
*
|
||||
* @returns {Promise<IConnectionCredentialsQuickPickItem[]>}
|
||||
*/
|
||||
public getPickListItems(): Promise<IConnectionCredentialsQuickPickItem[]> {
|
||||
const self = this;
|
||||
return new Promise<IConnectionCredentialsQuickPickItem[]>((resolve, reject) => {
|
||||
self.loadAllConnections()
|
||||
.then((pickListItems: IConnectionCredentialsQuickPickItem[]) => {
|
||||
// Always add an "Add New Connection" quickpick item
|
||||
pickListItems.push(<IConnectionCredentialsQuickPickItem> {
|
||||
label: Constants.CreateProfileLabel,
|
||||
connectionCreds: undefined,
|
||||
quickPickItemType: CredentialsQuickPickItemType.NewConnection
|
||||
});
|
||||
resolve(pickListItems);
|
||||
});
|
||||
public getPickListItems(): IConnectionCredentialsQuickPickItem[] {
|
||||
let pickListItems: IConnectionCredentialsQuickPickItem[] = this.loadAllConnections();
|
||||
pickListItems.push(<IConnectionCredentialsQuickPickItem> {
|
||||
label: Constants.CreateProfileLabel,
|
||||
connectionCreds: undefined,
|
||||
quickPickItemType: CredentialsQuickPickItemType.NewConnection
|
||||
});
|
||||
return pickListItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all connection profiles stored in the user settings
|
||||
* Note: connections will not include password value
|
||||
*
|
||||
* @returns {Promise<IConnectionCredentialsQuickPickItem[]>}
|
||||
* @returns {IConnectionCredentialsQuickPickItem[]}
|
||||
*/
|
||||
public getProfilePickListItems(): Promise<IConnectionCredentialsQuickPickItem[]> {
|
||||
const self = this;
|
||||
return self.loadProfiles().then(items => {
|
||||
// TODO add MRU list here
|
||||
return items;
|
||||
});
|
||||
public getProfilePickListItems(): IConnectionCredentialsQuickPickItem[] {
|
||||
return this.loadProfiles();
|
||||
}
|
||||
|
||||
public addSavedPassword(credentialsItem: IConnectionCredentialsQuickPickItem): Promise<IConnectionCredentialsQuickPickItem> {
|
||||
|
@ -164,7 +156,7 @@ export class ConnectionStore {
|
|||
self._context.globalState.update(Constants.configMyConnections, configValues)
|
||||
.then(() => {
|
||||
// Only save if we successfully added the profile
|
||||
return self.savePasswordIfNeeded(profile);
|
||||
return self.saveProfilePasswordIfNeeded(profile);
|
||||
// And resolve / reject at the end of the process
|
||||
}, err => {
|
||||
reject(err);
|
||||
|
@ -179,12 +171,72 @@ export class ConnectionStore {
|
|||
});
|
||||
}
|
||||
|
||||
private savePasswordIfNeeded(profile: IConnectionProfile): Promise<boolean> {
|
||||
/**
|
||||
* Gets the list of recently used connections. These will not include the password - a separate call to
|
||||
* {addSavedPassword} is needed to fill that before connecting
|
||||
*
|
||||
* @returns {IConnectionCredentials[]} the array of connections, empty if none are found
|
||||
*/
|
||||
public getRecentlyUsedConnections(): IConnectionCredentials[] {
|
||||
let configValues = this._context.globalState.get<IConnectionCredentials[]>(Constants.configRecentConnections);
|
||||
if (!configValues) {
|
||||
configValues = [];
|
||||
}
|
||||
return configValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a connection to the recently used list.
|
||||
* Password values are stored to a separate credential store if the "savePassword" option is true
|
||||
*
|
||||
* @param {IConnectionCredentials} conn the connection to add
|
||||
* @returns {Promise<void>} a Promise that returns when the connection was saved
|
||||
*/
|
||||
public addRecentlyUsed(conn: IConnectionCredentials): Promise<void> {
|
||||
const self = this;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// Get all profiles
|
||||
let configValues = self.getRecentlyUsedConnections();
|
||||
let maxConnections = self.getMaxRecentConnectionsCount();
|
||||
|
||||
// Remove the connection from the list if it already exists
|
||||
configValues = configValues.filter(value => !Utils.isSameConnection(value, conn));
|
||||
|
||||
// Add the connection to the front of the list, taking care to clear out the password field
|
||||
let savedConn: IConnectionCredentials = Object.assign({}, conn, { password: '' });
|
||||
configValues.unshift(savedConn);
|
||||
|
||||
// Remove last element if needed
|
||||
if (configValues.length > maxConnections) {
|
||||
configValues = configValues.slice(0, maxConnections);
|
||||
}
|
||||
|
||||
self._context.globalState.update(Constants.configRecentConnections, configValues)
|
||||
.then(() => {
|
||||
// Only save if we successfully added the profile
|
||||
self.doSavePassword(conn, CredentialsQuickPickItemType.Mru);
|
||||
// And resolve / reject at the end of the process
|
||||
resolve(undefined);
|
||||
}, err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private saveProfilePasswordIfNeeded(profile: IConnectionProfile): Promise<boolean> {
|
||||
if (!profile.savePassword) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return this.doSavePassword(profile, CredentialsQuickPickItemType.Profile);
|
||||
}
|
||||
|
||||
private doSavePassword(conn: IConnectionCredentials, type: CredentialsQuickPickItemType): Promise<boolean> {
|
||||
let self = this;
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
if (profile.savePassword === true && Utils.isNotEmpty(profile.password)) {
|
||||
let credentialId = ConnectionStore.formatCredentialId(profile.server, profile.database, profile.user, ConnectionStore.CRED_PROFILE_USER);
|
||||
self._credentialStore.saveCredential(credentialId, profile.password)
|
||||
if (Utils.isNotEmpty(conn.password)) {
|
||||
let credType: string = type === CredentialsQuickPickItemType.Mru ? ConnectionStore.CRED_MRU_USER : ConnectionStore.CRED_PROFILE_USER;
|
||||
let credentialId = ConnectionStore.formatCredentialId(conn.server, conn.database, conn.user, credType);
|
||||
self._credentialStore.saveCredential(credentialId, conn.password)
|
||||
.then((result) => {
|
||||
resolve(result);
|
||||
}).catch(err => {
|
||||
|
@ -192,7 +244,6 @@ export class ConnectionStore {
|
|||
reject(err);
|
||||
});
|
||||
} else {
|
||||
// Operation successful as didn't need to save
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
|
@ -252,40 +303,60 @@ export class ConnectionStore {
|
|||
}
|
||||
|
||||
// Load connections from user preferences
|
||||
private loadAllConnections(): Promise<IConnectionCredentialsQuickPickItem[]> {
|
||||
let self = this;
|
||||
return new Promise<IConnectionCredentialsQuickPickItem[]>(resolve => {
|
||||
// Load connections from user preferences
|
||||
// Per this https://code.visualstudio.com/Docs/customization/userandworkspace
|
||||
// Settings defined in workspace scope overwrite the settings defined in user scope
|
||||
let connections: IConnectionCredentials[] = [];
|
||||
let config = vscode.workspace.getConfiguration(Constants.extensionName);
|
||||
private loadAllConnections(): IConnectionCredentialsQuickPickItem[] {
|
||||
let quickPickItems: IConnectionCredentialsQuickPickItem[] = [];
|
||||
|
||||
// first read from the user settings
|
||||
let configValues = config[Constants.configMyConnections];
|
||||
self.addConnections(connections, configValues);
|
||||
let quickPickItems = connections.map(c => self.createQuickPickItem(c, CredentialsQuickPickItemType.Profile));
|
||||
resolve(quickPickItems);
|
||||
}).then(quickPickItems => {
|
||||
// next read from the global state
|
||||
let allQuickPickItems = self.loadProfiles().then(items => {
|
||||
return quickPickItems.concat(items);
|
||||
});
|
||||
// Read recently used items from a memento
|
||||
let recentConnections = this.getConnectionsFromGlobalState(Constants.configRecentConnections);
|
||||
quickPickItems = quickPickItems.concat(this.mapToQuickPickItems(recentConnections, CredentialsQuickPickItemType.Mru));
|
||||
|
||||
return allQuickPickItems;
|
||||
});
|
||||
// Load connections from user preferences
|
||||
// Per this https://code.visualstudio.com/Docs/customization/userandworkspace
|
||||
// Settings defined in workspace scope overwrite the settings defined in user scope
|
||||
let profilesInConfiguration = this.getConnectionsFromConfig<IConnectionCredentials>(Constants.configMyConnections);
|
||||
quickPickItems = quickPickItems.concat(this.mapToQuickPickItems(profilesInConfiguration, CredentialsQuickPickItemType.Profile));
|
||||
|
||||
// next read from the profiles saved in our own memento
|
||||
// TODO remove once user settings are editable programmatically
|
||||
let profiles = this.loadProfiles();
|
||||
quickPickItems = quickPickItems.concat(profiles);
|
||||
|
||||
// Return all connections
|
||||
return quickPickItems;
|
||||
}
|
||||
|
||||
private loadProfiles(): Promise<IConnectionCredentialsQuickPickItem[]> {
|
||||
let self = this;
|
||||
return new Promise<IConnectionCredentialsQuickPickItem[]>((resolve, reject) => {
|
||||
let connections: IConnectionProfile[] = [];
|
||||
// read from the global state
|
||||
let configValues = self._context.globalState.get<IConnectionProfile[]>(Constants.configMyConnections);
|
||||
self.addConnections(connections, configValues);
|
||||
let quickPickItems = connections.map(c => self.createQuickPickItem(c, CredentialsQuickPickItemType.Profile));
|
||||
resolve(quickPickItems);
|
||||
});
|
||||
private getConnectionsFromGlobalState<T extends IConnectionCredentials>(configName: string): T[] {
|
||||
let connections: T[] = [];
|
||||
// read from the global state
|
||||
let configValues = this._context.globalState.get<T[]>(configName);
|
||||
this.addConnections(connections, configValues);
|
||||
return connections;
|
||||
}
|
||||
|
||||
private getConnectionsFromConfig<T extends IConnectionCredentials>(configName: string): T[] {
|
||||
let config = this._vscodeWrapper.getConfiguration(Constants.extensionName);
|
||||
// we do not want the default value returned since it's used for helping users only
|
||||
let configValues = config.get(configName, undefined);
|
||||
if (configValues) {
|
||||
configValues = configValues.filter(conn => {
|
||||
// filter any connection missing a server name or the sample that's shown by default
|
||||
return !!(conn.server) && conn.server !== Constants.SampleServerName;
|
||||
});
|
||||
} else {
|
||||
configValues = [];
|
||||
}
|
||||
return configValues;
|
||||
}
|
||||
|
||||
|
||||
private mapToQuickPickItems(connections: IConnectionCredentials[], itemType: CredentialsQuickPickItemType): IConnectionCredentialsQuickPickItem[] {
|
||||
return connections.map(c => this.createQuickPickItem(c, itemType));
|
||||
}
|
||||
|
||||
private loadProfiles(): IConnectionCredentialsQuickPickItem[] {
|
||||
let connections: IConnectionProfile[] = this.getConnectionsFromGlobalState<IConnectionProfile>(Constants.configMyConnections);
|
||||
let quickPickItems = connections.map(c => this.createQuickPickItem(c, CredentialsQuickPickItemType.Profile));
|
||||
return quickPickItems;
|
||||
}
|
||||
|
||||
private addConnections(connections: IConnectionCredentials[], configValues: IConnectionCredentials[]): void {
|
||||
|
@ -301,4 +372,14 @@ export class ConnectionStore {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getMaxRecentConnectionsCount(): number {
|
||||
let config = this._vscodeWrapper.getConfiguration(Constants.extensionName);
|
||||
|
||||
let maxConnections: number = config[Constants.configMaxRecentConnections];
|
||||
if (typeof(maxConnections) !== 'number' || maxConnections <= 0) {
|
||||
maxConnections = 5;
|
||||
}
|
||||
return maxConnections;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,8 @@ export const msgContentProviderSqlOutputHtml = 'sqlOutput.ejs';
|
|||
export const configLogDebugInfo = 'logDebugInfo';
|
||||
export const configMyConnections = 'connections';
|
||||
export const configSaveAsCsv = 'saveAsCsv';
|
||||
export const configRecentConnections = 'recentConnections';
|
||||
export const configMaxRecentConnections = 'maxRecentConnections';
|
||||
|
||||
// localizable strings
|
||||
export const configMyConnectionsNoServerName = 'Missing server name in user preferences connection: ';
|
||||
|
@ -72,6 +74,7 @@ export const labelOpenGlobalSettings = 'Open Global Settings';
|
|||
export const labelOpenWorkspaceSettings = 'Open Workspace Settings';
|
||||
export const CreateProfileLabel = 'Create Connection Profile';
|
||||
export const RemoveProfileLabel = 'Remove Connection Profile';
|
||||
export const SampleServerName = '{{put-server-name-here}}';
|
||||
|
||||
export const serverPrompt = 'Server name';
|
||||
export const serverPlaceholder = 'hostname\\instance or <server>.database.windows.net';
|
||||
|
|
|
@ -195,6 +195,23 @@ export function isSameProfile(currentProfile: interfaces.IConnectionProfile, exp
|
|||
&& expectedProfile.user === currentProfile.user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares 2 connections to see if they match. Logic for matching:
|
||||
* match on all key properties (server, db, auth type, user) being identical.
|
||||
* Other properties are ignored for this purpose
|
||||
*
|
||||
* @param {IConnectionCredentials} conn the connection to check
|
||||
* @param {IConnectionCredentials} expectedConn the connection to try to match
|
||||
* @returns boolean that is true if the connections match
|
||||
*/
|
||||
export function isSameConnection(conn: interfaces.IConnectionCredentials, expectedConn: interfaces.IConnectionCredentials): boolean {
|
||||
return expectedConn.server === conn.server
|
||||
&& expectedConn.database === conn.database
|
||||
&& expectedConn.authenticationType === conn.authenticationType
|
||||
&& expectedConn.user === conn.user;
|
||||
}
|
||||
|
||||
|
||||
// One-time use timer for performance testing
|
||||
export class Timer {
|
||||
private _startTime: number[];
|
||||
|
|
|
@ -40,27 +40,25 @@ export class ConnectionUI {
|
|||
public showConnections(): Promise<IConnectionCredentials> {
|
||||
const self = this;
|
||||
return new Promise<IConnectionCredentials>((resolve, reject) => {
|
||||
self._connectionStore.getPickListItems()
|
||||
.then((picklist: IConnectionCredentialsQuickPickItem[]) => {
|
||||
if (picklist.length === 0) {
|
||||
// No recent connections - prompt to open user settings or workspace settings to add a connection
|
||||
self.openUserOrWorkspaceSettings();
|
||||
resolve(undefined);
|
||||
} else {
|
||||
// We have recent connections - show them in a picklist
|
||||
self.promptItemChoice({
|
||||
placeHolder: Constants.recentConnectionsPlaceholder,
|
||||
matchOnDescription: true
|
||||
}, picklist)
|
||||
.then(selection => {
|
||||
if (selection) {
|
||||
resolve(self.handleSelectedConnection(selection));
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
let picklist: IConnectionCredentialsQuickPickItem[] = self._connectionStore.getPickListItems();
|
||||
if (picklist.length === 0) {
|
||||
// No recent connections - prompt to open user settings or workspace settings to add a connection
|
||||
self.openUserOrWorkspaceSettings();
|
||||
resolve(undefined);
|
||||
} else {
|
||||
// We have recent connections - show them in a picklist
|
||||
self.promptItemChoice({
|
||||
placeHolder: Constants.recentConnectionsPlaceholder,
|
||||
matchOnDescription: true
|
||||
}, picklist)
|
||||
.then(selection => {
|
||||
if (selection) {
|
||||
resolve(self.handleSelectedConnection(selection));
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -239,20 +237,20 @@ export class ConnectionUI {
|
|||
let self = this;
|
||||
|
||||
// Flow: Select profile to remove, confirm removal, remove, notify
|
||||
return self._connectionStore.getProfilePickListItems()
|
||||
.then(profiles => self.selectProfileForRemoval(profiles))
|
||||
.then(profile => {
|
||||
if (profile) {
|
||||
return self._connectionStore.removeProfile(profile);
|
||||
}
|
||||
return false;
|
||||
}).then(result => {
|
||||
if (result) {
|
||||
// TODO again consider moving information prompts to the prompt package
|
||||
vscode.window.showInformationMessage(Constants.msgProfileRemoved);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
let profiles = self._connectionStore.getProfilePickListItems();
|
||||
return self.selectProfileForRemoval(profiles)
|
||||
.then(profile => {
|
||||
if (profile) {
|
||||
return self._connectionStore.removeProfile(profile);
|
||||
}
|
||||
return false;
|
||||
}).then(result => {
|
||||
if (result) {
|
||||
// TODO again consider moving information prompts to the prompt package
|
||||
vscode.window.showInformationMessage(Constants.msgProfileRemoved);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private selectProfileForRemoval(profiles: IConnectionCredentialsQuickPickItem[]): Promise<IConnectionProfile> {
|
||||
|
|
|
@ -5,44 +5,59 @@ import * as TypeMoq from 'typemoq';
|
|||
import vscode = require('vscode');
|
||||
import * as utils from '../src/models/utils';
|
||||
import * as connectionInfo from '../src/models/connectionInfo';
|
||||
import { TestExtensionContext, TestMemento } from './stubs';
|
||||
import { IConnectionProfile, CredentialsQuickPickItemType, AuthenticationTypes } from '../src/models/interfaces';
|
||||
import * as Constants from '../src/models/constants';
|
||||
import * as stubs from './stubs';
|
||||
import * as interfaces from '../src/models/interfaces';
|
||||
import { CredentialStore } from '../src/credentialstore/credentialstore';
|
||||
import { ConnectionProfile } from '../src/models/connectionProfile';
|
||||
import { ConnectionStore } from '../src/models/connectionStore';
|
||||
import VscodeWrapper from '../src/controllers/vscodeWrapper';
|
||||
|
||||
import assert = require('assert');
|
||||
|
||||
suite('ConnectionStore tests', () => {
|
||||
let defaultNamedProfile: IConnectionProfile;
|
||||
let defaultUnnamedProfile: IConnectionProfile;
|
||||
let defaultNamedProfile: interfaces.IConnectionProfile;
|
||||
let defaultUnnamedProfile: interfaces.IConnectionProfile;
|
||||
let context: TypeMoq.Mock<vscode.ExtensionContext>;
|
||||
let globalstate: TypeMoq.Mock<vscode.Memento>;
|
||||
let credentialStore: TypeMoq.Mock<CredentialStore>;
|
||||
let vscodeWrapper: TypeMoq.Mock<VscodeWrapper>;
|
||||
|
||||
setup(() => {
|
||||
defaultNamedProfile = Object.assign(new ConnectionProfile(), {
|
||||
profileName: 'abc-bcd-cde',
|
||||
server: 'abc',
|
||||
profileName: 'defaultNamedProfile',
|
||||
server: 'namedServer',
|
||||
database: 'bcd',
|
||||
authenticationType: utils.authTypeToString(AuthenticationTypes.SqlLogin),
|
||||
authenticationType: utils.authTypeToString(interfaces.AuthenticationTypes.SqlLogin),
|
||||
user: 'cde',
|
||||
password: 'asdf!@#$'
|
||||
});
|
||||
|
||||
defaultUnnamedProfile = Object.assign(new ConnectionProfile(), {
|
||||
profileName: undefined,
|
||||
server: 'namedServer',
|
||||
server: 'unnamedServer',
|
||||
database: undefined,
|
||||
authenticationType: utils.authTypeToString(AuthenticationTypes.SqlLogin),
|
||||
authenticationType: utils.authTypeToString(interfaces.AuthenticationTypes.SqlLogin),
|
||||
user: 'aUser',
|
||||
password: 'asdf!@#$'
|
||||
});
|
||||
|
||||
context = TypeMoq.Mock.ofType(TestExtensionContext);
|
||||
globalstate = TypeMoq.Mock.ofType(TestMemento);
|
||||
context = TypeMoq.Mock.ofType(stubs.TestExtensionContext);
|
||||
globalstate = TypeMoq.Mock.ofType(stubs.TestMemento);
|
||||
context.object.globalState = globalstate.object;
|
||||
credentialStore = TypeMoq.Mock.ofType(CredentialStore);
|
||||
vscodeWrapper = TypeMoq.Mock.ofType(VscodeWrapper);
|
||||
|
||||
// setup default behavior for vscodeWrapper
|
||||
// setup configuration to return maxRecent for the #MRU items
|
||||
let maxRecent = 5;
|
||||
let configResult: {[key: string]: any} = {};
|
||||
configResult[Constants.configMaxRecentConnections] = maxRecent;
|
||||
let config = stubs.createWorkspaceConfiguration(configResult);
|
||||
vscodeWrapper.setup(x => x.getConfiguration(TypeMoq.It.isAny()))
|
||||
.returns(x => {
|
||||
return config;
|
||||
});
|
||||
});
|
||||
|
||||
test('formatCredentialId should handle server, DB and username correctly', () => {
|
||||
|
@ -76,7 +91,7 @@ suite('ConnectionStore tests', () => {
|
|||
user: 'cde',
|
||||
password: 'asdf!@#$'
|
||||
});
|
||||
let label = connectionInfo.getPicklistLabel(unnamedProfile, CredentialsQuickPickItemType.Profile);
|
||||
let label = connectionInfo.getPicklistLabel(unnamedProfile, interfaces.CredentialsQuickPickItemType.Profile);
|
||||
assert.ok(label.endsWith(unnamedProfile.server));
|
||||
});
|
||||
|
||||
|
@ -88,13 +103,13 @@ suite('ConnectionStore tests', () => {
|
|||
user: 'cde',
|
||||
password: 'asdf!@#$'
|
||||
});
|
||||
let label = connectionInfo.getPicklistLabel(namedProfile, CredentialsQuickPickItemType.Profile);
|
||||
let label = connectionInfo.getPicklistLabel(namedProfile, interfaces.CredentialsQuickPickItemType.Profile);
|
||||
assert.ok(label.endsWith(namedProfile.profileName));
|
||||
});
|
||||
|
||||
test('getPickListLabel has different symbols for Profiles vs Recently Used', () => {
|
||||
let profileLabel: string = connectionInfo.getPicklistLabel(defaultNamedProfile, CredentialsQuickPickItemType.Profile);
|
||||
let mruLabel: string = connectionInfo.getPicklistLabel(defaultNamedProfile, CredentialsQuickPickItemType.Mru);
|
||||
let profileLabel: string = connectionInfo.getPicklistLabel(defaultNamedProfile, interfaces.CredentialsQuickPickItemType.Profile);
|
||||
let mruLabel: string = connectionInfo.getPicklistLabel(defaultNamedProfile, interfaces.CredentialsQuickPickItemType.Mru);
|
||||
|
||||
assert.ok(mruLabel, 'expect value for label');
|
||||
assert.ok(profileLabel, 'expect value for label');
|
||||
|
@ -105,9 +120,9 @@ suite('ConnectionStore tests', () => {
|
|||
// Given
|
||||
globalstate.setup(x => x.get(TypeMoq.It.isAny())).returns(key => []);
|
||||
|
||||
let credsToSave: IConnectionProfile[];
|
||||
let credsToSave: interfaces.IConnectionProfile[];
|
||||
globalstate.setup(x => x.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyObject(Array)))
|
||||
.returns((id: string, profiles: IConnectionProfile[]) => {
|
||||
.returns((id: string, profiles: interfaces.IConnectionProfile[]) => {
|
||||
credsToSave = profiles;
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
@ -115,7 +130,7 @@ suite('ConnectionStore tests', () => {
|
|||
let connectionStore = new ConnectionStore(context.object, credentialStore.object);
|
||||
|
||||
// When SaveProfile is called with savePassword false
|
||||
let profile: IConnectionProfile = Object.assign(new ConnectionProfile(), defaultNamedProfile, { savePassword: false });
|
||||
let profile: interfaces.IConnectionProfile = Object.assign(new ConnectionProfile(), defaultNamedProfile, { savePassword: false });
|
||||
return connectionStore.saveProfile(profile)
|
||||
.then(savedProfile => {
|
||||
// Then expect password not saved in either the context object or the credential store
|
||||
|
@ -131,29 +146,29 @@ suite('ConnectionStore tests', () => {
|
|||
// Given
|
||||
globalstate.setup(x => x.get(TypeMoq.It.isAny())).returns(key => []);
|
||||
|
||||
let credsToSave: IConnectionProfile[];
|
||||
let credsToSave: interfaces.IConnectionProfile[];
|
||||
globalstate.setup(x => x.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyObject(Array)))
|
||||
.returns((id: string, profiles: IConnectionProfile[]) => {
|
||||
.returns((id: string, profiles: interfaces.IConnectionProfile[]) => {
|
||||
credsToSave = profiles;
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
let capturedCreds: any;
|
||||
credentialStore.setup(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.callback((cred: string, pass: any) => {
|
||||
capturedCreds = {
|
||||
'credentialId': cred,
|
||||
'password': pass
|
||||
};
|
||||
})
|
||||
.returns(() => Promise.resolve(true));
|
||||
.callback((cred: string, pass: any) => {
|
||||
capturedCreds = {
|
||||
'credentialId': cred,
|
||||
'password': pass
|
||||
};
|
||||
})
|
||||
.returns(() => Promise.resolve(true));
|
||||
|
||||
let expectedCredFormat: string = ConnectionStore.formatCredentialId(defaultNamedProfile.server, defaultNamedProfile.database, defaultNamedProfile.user);
|
||||
|
||||
let connectionStore = new ConnectionStore(context.object, credentialStore.object);
|
||||
|
||||
// When SaveProfile is called with savePassword true
|
||||
let profile: IConnectionProfile = Object.assign(new ConnectionProfile(), defaultNamedProfile, { savePassword: true });
|
||||
let profile: interfaces.IConnectionProfile = Object.assign(new ConnectionProfile(), defaultNamedProfile, { savePassword: true });
|
||||
|
||||
connectionStore.saveProfile(profile)
|
||||
.then(savedProfile => {
|
||||
|
@ -178,9 +193,9 @@ suite('ConnectionStore tests', () => {
|
|||
});
|
||||
globalstate.setup(x => x.get(TypeMoq.It.isAny())).returns(key => [defaultNamedProfile, profile]);
|
||||
|
||||
let updatedCredentials: IConnectionProfile[];
|
||||
let updatedCredentials: interfaces.IConnectionProfile[];
|
||||
globalstate.setup(x => x.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyObject(Array)))
|
||||
.returns((id: string, profiles: IConnectionProfile[]) => {
|
||||
.returns((id: string, profiles: interfaces.IConnectionProfile[]) => {
|
||||
updatedCredentials = profiles;
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
@ -226,9 +241,9 @@ suite('ConnectionStore tests', () => {
|
|||
});
|
||||
globalstate.setup(x => x.get(TypeMoq.It.isAny())).returns(key => [defaultNamedProfile, unnamedProfile, namedProfile]);
|
||||
|
||||
let updatedCredentials: IConnectionProfile[];
|
||||
let updatedCredentials: interfaces.IConnectionProfile[];
|
||||
globalstate.setup(x => x.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyObject(Array)))
|
||||
.returns((id: string, profiles: IConnectionProfile[]) => {
|
||||
.returns((id: string, profiles: interfaces.IConnectionProfile[]) => {
|
||||
updatedCredentials = profiles;
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
@ -261,9 +276,9 @@ suite('ConnectionStore tests', () => {
|
|||
});
|
||||
globalstate.setup(x => x.get(TypeMoq.It.isAny())).returns(key => [defaultNamedProfile, unnamedProfile, namedProfile]);
|
||||
|
||||
let updatedCredentials: IConnectionProfile[];
|
||||
let updatedCredentials: interfaces.IConnectionProfile[];
|
||||
globalstate.setup(x => x.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyObject(Array)))
|
||||
.returns((id: string, profiles: IConnectionProfile[]) => {
|
||||
.returns((id: string, profiles: interfaces.IConnectionProfile[]) => {
|
||||
updatedCredentials = profiles;
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
@ -283,5 +298,194 @@ suite('ConnectionStore tests', () => {
|
|||
done();
|
||||
}).catch(err => done(new Error(err)));
|
||||
});
|
||||
|
||||
test('addRecentlyUsed should limit saves to the MaxRecentConnections amount ', (done) => {
|
||||
// Given 3 is the max # creds
|
||||
let numCreds = 4;
|
||||
let maxRecent = 3;
|
||||
|
||||
// setup configuration to return maxRecent for the #MRU items - must override vscodeWrapper in this test
|
||||
vscodeWrapper = TypeMoq.Mock.ofType(VscodeWrapper);
|
||||
let configResult: {[key: string]: any} = {};
|
||||
configResult[Constants.configMaxRecentConnections] = maxRecent;
|
||||
let config = stubs.createWorkspaceConfiguration(configResult);
|
||||
vscodeWrapper.setup(x => x.getConfiguration(TypeMoq.It.isAny()))
|
||||
.returns(x => {
|
||||
return config;
|
||||
});
|
||||
|
||||
// setup memento for MRU to return a list we have access to
|
||||
let creds: interfaces.IConnectionCredentials[] = [];
|
||||
globalstate.setup(x => x.get(TypeMoq.It.isAny())).returns(key => creds.slice(0, creds.length));
|
||||
globalstate.setup(x => x.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyObject(Array)))
|
||||
.returns((id: string, credsToSave: interfaces.IConnectionCredentials[]) => {
|
||||
creds = credsToSave;
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
// When saving 4 connections
|
||||
// Then expect the only the 3 most recently saved connections to be returned as size is limited to 3
|
||||
let connectionStore = new ConnectionStore(context.object, credentialStore.object, vscodeWrapper.object);
|
||||
|
||||
let promise = Promise.resolve();
|
||||
for (let i = 0; i < numCreds; i++) {
|
||||
let cred = Object.assign({}, defaultNamedProfile, { server: defaultNamedProfile.server + i});
|
||||
promise = promise.then(() => {
|
||||
return connectionStore.addRecentlyUsed(cred);
|
||||
}).then(() => {
|
||||
if (i < maxRecent) {
|
||||
assert.equal(creds.length, i + 1, 'expect all credentials to be saved when limit not reached');
|
||||
} else {
|
||||
assert.equal(creds.length, maxRecent, `expect only top ${maxRecent} creds to be saved`);
|
||||
}
|
||||
assert.equal(creds[0].server, cred.server, 'Expect most recently saved item to be first in list');
|
||||
assert.ok(utils.isEmpty(creds[0].password));
|
||||
});
|
||||
}
|
||||
promise.then(() => {
|
||||
credentialStore.verify(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(numCreds));
|
||||
let recentConnections = connectionStore.getRecentlyUsedConnections();
|
||||
assert.equal(maxRecent, recentConnections.length);
|
||||
done();
|
||||
}, err => {
|
||||
// Must call done here so test indicates it's finished if errors occur
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
test('addRecentlyUsed should add same connection exactly once', (done) => {
|
||||
// setup memento for MRU to return a list we have access to
|
||||
let creds: interfaces.IConnectionCredentials[] = [];
|
||||
globalstate.setup(x => x.get(TypeMoq.It.isAny())).returns(key => creds.slice(0, creds.length));
|
||||
globalstate.setup(x => x.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyObject(Array)))
|
||||
.returns((id: string, credsToSave: interfaces.IConnectionCredentials[]) => {
|
||||
creds = credsToSave;
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
// Given we save the same connection twice
|
||||
// Then expect the only 1 instance of that connection to be listed in the MRU
|
||||
let connectionStore = new ConnectionStore(context.object, credentialStore.object, vscodeWrapper.object);
|
||||
|
||||
let promise = Promise.resolve();
|
||||
let cred = Object.assign({}, defaultNamedProfile, { server: defaultNamedProfile.server + 1});
|
||||
promise = promise.then(() => {
|
||||
return connectionStore.addRecentlyUsed(defaultNamedProfile);
|
||||
}).then(() => {
|
||||
return connectionStore.addRecentlyUsed(cred);
|
||||
}).then(() => {
|
||||
return connectionStore.addRecentlyUsed(cred);
|
||||
}).then(() => {
|
||||
assert.equal(creds.length, 2, 'expect 2 unique credentials to have been added');
|
||||
assert.equal(creds[0].server, cred.server, 'Expect most recently saved item to be first in list');
|
||||
assert.ok(utils.isEmpty(creds[0].password));
|
||||
}).then(() => done(), err => done(err));
|
||||
});
|
||||
|
||||
test('addRecentlyUsed should save password to credential store', (done) => {
|
||||
// setup memento for MRU to return a list we have access to
|
||||
let creds: interfaces.IConnectionCredentials[] = [];
|
||||
globalstate.setup(x => x.get(TypeMoq.It.isAny())).returns(key => creds.slice(0, creds.length));
|
||||
globalstate.setup(x => x.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyObject(Array)))
|
||||
.returns((id: string, credsToSave: interfaces.IConnectionCredentials[]) => {
|
||||
creds = credsToSave;
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
// Setup credential store to capture credentials sent to it
|
||||
let capturedCreds: any;
|
||||
credentialStore.setup(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.callback((cred: string, pass: any) => {
|
||||
capturedCreds = {
|
||||
'credentialId': cred,
|
||||
'password': pass
|
||||
};
|
||||
})
|
||||
.returns(() => Promise.resolve(true));
|
||||
|
||||
// Given we save 1 connection with password and multiple other connections without
|
||||
let connectionStore = new ConnectionStore(context.object, credentialStore.object, vscodeWrapper.object);
|
||||
let integratedCred = Object.assign({}, defaultNamedProfile, {
|
||||
server: defaultNamedProfile.server + 'Integrated',
|
||||
authenticationType: interfaces.AuthenticationTypes[interfaces.AuthenticationTypes.Integrated],
|
||||
user: '',
|
||||
password: ''
|
||||
});
|
||||
let noPwdCred = Object.assign({}, defaultNamedProfile, {
|
||||
server: defaultNamedProfile.server + 'NoPwd',
|
||||
password: ''
|
||||
});
|
||||
|
||||
let expectedCredCount = 0;
|
||||
let promise = Promise.resolve();
|
||||
promise = promise.then(() => {
|
||||
expectedCredCount++;
|
||||
return connectionStore.addRecentlyUsed(defaultNamedProfile);
|
||||
}).then(() => {
|
||||
// Then verify that since its password based we save the password
|
||||
credentialStore.verify(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
assert.strictEqual(capturedCreds.password, defaultNamedProfile.password);
|
||||
let credId: string = capturedCreds.credentialId;
|
||||
assert.ok(credId.includes(ConnectionStore.CRED_MRU_USER), 'Expect credential to be marked as an MRU cred');
|
||||
assert.ok(utils.isEmpty(creds[0].password));
|
||||
}).then(() => {
|
||||
// When add integrated auth connection
|
||||
expectedCredCount++;
|
||||
return connectionStore.addRecentlyUsed(integratedCred);
|
||||
}).then(() => {
|
||||
// then expect no to have credential store called, but MRU count upped to 2
|
||||
credentialStore.verify(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
assert.equal(creds.length, expectedCredCount, `expect ${expectedCredCount} unique credentials to have been added`);
|
||||
}).then(() => {
|
||||
// When add connection without password
|
||||
expectedCredCount++;
|
||||
return connectionStore.addRecentlyUsed(noPwdCred);
|
||||
}).then(() => {
|
||||
// then expect no to have credential store called, but MRU count upped to 3
|
||||
credentialStore.verify(x => x.saveCredential(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
assert.equal(creds.length, expectedCredCount, `expect ${expectedCredCount} unique credentials to have been added`);
|
||||
}).then(() => done(), err => done(err));
|
||||
});
|
||||
|
||||
test('getPickListItems should display Recently Used then Profiles then New Connection', (done) => {
|
||||
// Given 3 items in MRU and 2 in Profile list
|
||||
let recentlyUsed: interfaces.IConnectionCredentials[] = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
recentlyUsed.push( Object.assign({}, defaultNamedProfile, { server: defaultNamedProfile.server + i}) );
|
||||
}
|
||||
globalstate.setup(x => x.get(Constants.configRecentConnections)).returns(key => recentlyUsed);
|
||||
|
||||
let profiles: interfaces.IConnectionProfile[] = [defaultNamedProfile, defaultUnnamedProfile];
|
||||
globalstate.setup(x => x.get(Constants.configMyConnections)).returns(key => profiles);
|
||||
|
||||
// When we get the list of available connection items
|
||||
|
||||
// Then expect MRU items first, then profile items, then a new connection item
|
||||
let connectionStore = new ConnectionStore(context.object, credentialStore.object, vscodeWrapper.object);
|
||||
|
||||
let items: interfaces.IConnectionCredentialsQuickPickItem[] = connectionStore.getPickListItems();
|
||||
let expectedCount = recentlyUsed.length + profiles.length + 1;
|
||||
assert.equal(items.length, expectedCount);
|
||||
|
||||
// Then expect recent items first
|
||||
let i = 0;
|
||||
for (let recentItem of recentlyUsed) {
|
||||
assert.equal(items[i].connectionCreds, recentItem);
|
||||
assert.equal(items[i].quickPickItemType, interfaces.CredentialsQuickPickItemType.Mru);
|
||||
i++;
|
||||
}
|
||||
// Then profile items
|
||||
for (let profile of profiles) {
|
||||
assert.equal(items[i].connectionCreds, profile);
|
||||
assert.equal(items[i].quickPickItemType, interfaces.CredentialsQuickPickItemType.Profile);
|
||||
i++;
|
||||
}
|
||||
// then new connection
|
||||
assert.equal(items[i].quickPickItemType, interfaces.CredentialsQuickPickItemType.NewConnection);
|
||||
|
||||
// Then test is complete
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { IConnectionCredentials, AuthenticationTypes } from '../src/models/inter
|
|||
import * as ConnectionContracts from '../src/models/contracts/connection';
|
||||
import MainController from '../src/controllers/controller';
|
||||
import * as Interfaces from '../src/models/interfaces';
|
||||
import { ConnectionStore } from '../src/models/connectionStore';
|
||||
import StatusView from '../src/views/statusView';
|
||||
import Telemetry from '../src/models/telemetry';
|
||||
import * as Utils from '../src/models/utils';
|
||||
|
@ -59,14 +60,23 @@ function createTestCredentials(): IConnectionCredentials {
|
|||
function createTestConnectionManager(
|
||||
serviceClient: SqlToolsServiceClient,
|
||||
wrapper?: VscodeWrapper,
|
||||
statusView?: StatusView): ConnectionManager {
|
||||
statusView?: StatusView,
|
||||
connectionStore?: ConnectionStore): ConnectionManager {
|
||||
|
||||
let contextMock: TypeMoq.Mock<ExtensionContext> = TypeMoq.Mock.ofType(TestExtensionContext);
|
||||
let prompterMock: TypeMoq.Mock<IPrompter> = TypeMoq.Mock.ofType(TestPrompter);
|
||||
if (!statusView) {
|
||||
statusView = TypeMoq.Mock.ofType(StatusView).object;
|
||||
}
|
||||
return new ConnectionManager(contextMock.object, statusView, prompterMock.object, serviceClient, wrapper);
|
||||
if (!connectionStore) {
|
||||
let connectionStoreMock = TypeMoq.Mock.ofType(ConnectionStore);
|
||||
// disable saving recently used by default
|
||||
connectionStoreMock.setup(x => x.addRecentlyUsed(TypeMoq.It.isAny())).returns(() => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
connectionStore = connectionStoreMock.object;
|
||||
}
|
||||
return new ConnectionManager(contextMock.object, statusView, prompterMock.object, serviceClient, wrapper, connectionStore);
|
||||
}
|
||||
|
||||
function createTestListDatabasesResult(): ConnectionContracts.ListDatabasesResult {
|
||||
|
@ -354,14 +364,7 @@ suite('Per File Connection Tests', () => {
|
|||
connectionCreds.database = '';
|
||||
|
||||
// When the result will return 'master' as the database connected to
|
||||
let myResult = new ConnectionContracts.ConnectionResult();
|
||||
myResult.connectionId = Utils.generateGuid();
|
||||
myResult.messages = '';
|
||||
myResult.connectionSummary = {
|
||||
serverName: connectionCreds.server,
|
||||
databaseName: expectedDbName,
|
||||
userName: connectionCreds.user
|
||||
};
|
||||
let myResult = createConnectionResultForCreds(connectionCreds, expectedDbName);
|
||||
|
||||
let serviceClientMock: TypeMoq.Mock<SqlToolsServiceClient> = TypeMoq.Mock.ofType(SqlToolsServiceClient);
|
||||
serviceClientMock.setup(x => x.sendRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
|
@ -387,4 +390,64 @@ suite('Per File Connection Tests', () => {
|
|||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
function createConnectionResultForCreds(connectionCreds: IConnectionCredentials, dbName?: string): ConnectionContracts.ConnectionResult {
|
||||
let myResult = new ConnectionContracts.ConnectionResult();
|
||||
if (!dbName) {
|
||||
dbName = connectionCreds.database;
|
||||
}
|
||||
myResult.connectionId = Utils.generateGuid();
|
||||
myResult.messages = '';
|
||||
myResult.connectionSummary = {
|
||||
serverName: connectionCreds.server,
|
||||
databaseName: dbName,
|
||||
userName: connectionCreds.user
|
||||
};
|
||||
return myResult;
|
||||
}
|
||||
|
||||
test('Should save new connections to recently used list', done => {
|
||||
const testFile = 'file:///my/test/file.sql';
|
||||
const expectedDbName = 'master';
|
||||
|
||||
// Given a connection to default database
|
||||
let connectionCreds = createTestCredentials();
|
||||
connectionCreds.database = '';
|
||||
|
||||
// When the result will return 'master' as the database connected to
|
||||
let myResult = createConnectionResultForCreds(connectionCreds, expectedDbName);
|
||||
|
||||
let serviceClientMock: TypeMoq.Mock<SqlToolsServiceClient> = TypeMoq.Mock.ofType(SqlToolsServiceClient);
|
||||
serviceClientMock.setup(x => x.sendRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns(() => Promise.resolve(myResult));
|
||||
|
||||
let statusViewMock: TypeMoq.Mock<StatusView> = TypeMoq.Mock.ofType(StatusView);
|
||||
let actualDbName = undefined;
|
||||
statusViewMock.setup(x => x.connectSuccess(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.callback((fileUri, creds: IConnectionCredentials) => {
|
||||
actualDbName = creds.database;
|
||||
});
|
||||
|
||||
// And we store any DBs saved to recent connections
|
||||
let savedConnection: IConnectionCredentials = undefined;
|
||||
let connectionStoreMock = TypeMoq.Mock.ofType(ConnectionStore);
|
||||
connectionStoreMock.setup(x => x.addRecentlyUsed(TypeMoq.It.isAny())).returns( conn => {
|
||||
savedConnection = conn;
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
let manager: ConnectionManager = createTestConnectionManager(serviceClientMock.object, undefined, statusViewMock.object, connectionStoreMock.object);
|
||||
|
||||
// Then on connecting expect 'master' to be the database used in status view and URI mapping
|
||||
manager.connect(testFile, connectionCreds).then( result => {
|
||||
assert.equal(result, true);
|
||||
connectionStoreMock.verify(x => x.addRecentlyUsed(TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
assert.equal(savedConnection.database, expectedDbName, 'Expect actual DB name returned from connection to be saved');
|
||||
assert.equal(savedConnection.password, connectionCreds.password, 'Expect password to be saved');
|
||||
|
||||
done();
|
||||
}).catch(err => {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -39,4 +39,26 @@ class TestMemento implements vscode.Memento {
|
|||
}
|
||||
}
|
||||
|
||||
export { TestPrompter, TestExtensionContext, TestMemento };
|
||||
function createWorkspaceConfiguration(items: {[key: string]: any}): vscode.WorkspaceConfiguration {
|
||||
const result: vscode.WorkspaceConfiguration = {
|
||||
has(key: string): boolean {
|
||||
return items[key] !== 'undefined';
|
||||
},
|
||||
get<T>(key: string, defaultValue?: T): T {
|
||||
let val = items[key];
|
||||
if (typeof val === 'undefined') {
|
||||
val = defaultValue;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
};
|
||||
|
||||
// Copy properties across so that indexer works as expected
|
||||
Object.keys(items).forEach((key) => {
|
||||
result[key] = items[key];
|
||||
});
|
||||
|
||||
return Object.freeze(result);
|
||||
}
|
||||
|
||||
export { TestPrompter, TestExtensionContext, TestMemento, createWorkspaceConfiguration };
|
||||
|
|
Загрузка…
Ссылка в новой задаче