diff --git a/extensions/sql-bindings/src/common/azureFunctionsUtils.ts b/extensions/sql-bindings/src/common/azureFunctionsUtils.ts index 966ff6160a..4f237f1fd2 100644 --- a/extensions/sql-bindings/src/common/azureFunctionsUtils.ts +++ b/extensions/sql-bindings/src/common/azureFunctionsUtils.ts @@ -7,7 +7,8 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as utils from './utils'; import * as constants from './constants'; -import { BindingType } from 'sql-bindings'; +import * as azureFunctionsContracts from '../contracts/azureFunctions/azureFunctionsContracts'; +import { BindingType, IConnectionStringInfo } from 'sql-bindings'; import { ConnectionDetails, IConnectionInfo } from 'vscode-mssql'; // https://github.com/microsoft/vscode-azurefunctions/blob/main/src/vscode-azurefunctions.api.d.ts import { AzureFunctionsExtensionApi } from '../../../types/vscode-azurefunctions.api'; @@ -313,23 +314,54 @@ export async function promptForBindingType(funcName?: string): Promise { - return vscode.window.showInputBox({ - prompt: bindingType === BindingType.input ? constants.sqlTableOrViewToQuery : constants.sqlTableToUpsert, - placeHolder: constants.placeHolderObject, - validateInput: input => input ? undefined : constants.nameMustNotBeEmpty, - ignoreFocusOut: true - }); +export async function promptForObjectName(bindingType: BindingType, connectionInfo?: IConnectionInfo): Promise { + // show the connection string methods (user input and connection profile options) + let connectionURI: string | undefined; + let selectedDatabase: string | undefined; + + while (true) { + if (!connectionInfo) { + // prompt is shown when user selects an existing connection string setting + // or manually enters a connection string + return promptToManuallyEnterObjectName(bindingType); + } + + // TODO create path to solve for selectView (first need to support views as well) + // Prompt user to select a table based on connection profile and selected database} + // get connectionURI and selectedDatabase to be used for listing tables query request + connectionURI = await getConnectionURI(connectionInfo); + if (!connectionURI) { + // User cancelled or mssql connection error + // we will then prompt user to choose a connection profile again + continue; + } + selectedDatabase = await promptSelectDatabase(connectionURI); + if (!selectedDatabase) { + // User cancelled + // we will then prompt user to choose a connection profile again + continue; + } + + connectionInfo.database = selectedDatabase; + + let selectedObjectName = await promptSelectTable(connectionURI, bindingType, selectedDatabase); + + return selectedObjectName; + } } /** * Prompts the user to enter connection setting and updates it from AF project * @param projectUri Azure Function project uri - * @param connectionInfo connection info from the user to update the connection string + * @param connectionInfo (optional) connection info from the user to update the connection string, + * if left undefined we prompt the user for the connection info * @returns connection string setting name to be used for the createFunction API */ -export async function promptAndUpdateConnectionStringSetting(projectUri: vscode.Uri | undefined, connectionInfo?: IConnectionInfo): Promise { +export async function promptAndUpdateConnectionStringSetting(projectUri: vscode.Uri | undefined, connectionInfo?: IConnectionInfo): Promise { let connectionStringSettingName: string | undefined; const vscodeMssqlApi = await utils.getVscodeMssqlApi(); @@ -459,6 +491,11 @@ export async function promptAndUpdateConnectionStringSetting(projectUri: vscode. validateInput: input => input ? undefined : constants.valueMustNotBeEmpty } ) ?? ''; + if (!connectionString) { + // User cancelled + // we can prompt for connection string methods again + continue; + } } else { // Let user choose from existing connections to create connection string from connectionInfo = await vscodeMssqlApi.promptForConnection(true); @@ -509,7 +546,7 @@ export async function promptAndUpdateConnectionStringSetting(projectUri: vscode. ignoreFocusOut: true }); } - return connectionStringSettingName; + return { connectionStringSettingName: connectionStringSettingName!, connectionInfo: connectionInfo }; } /** @@ -579,10 +616,9 @@ export async function promptConnectionStringPasswordAndUpdateConnectionString(co } } -export async function promptSelectDatabase(connectionInfo: IConnectionInfo): Promise { +export async function promptSelectDatabase(connectionURI: string): Promise { const vscodeMssqlApi = await utils.getVscodeMssqlApi(); - let connectionURI = await vscodeMssqlApi.connect(connectionInfo); let listDatabases = await vscodeMssqlApi.listDatabases(connectionURI); const selectedDatabase = (await vscode.window.showQuickPick(listDatabases, { canPickMany: false, @@ -596,3 +632,77 @@ export async function promptSelectDatabase(connectionInfo: IConnectionInfo): Pro } return selectedDatabase; } + +export async function getConnectionURI(connectionInfo: IConnectionInfo): Promise { + const vscodeMssqlApi = await utils.getVscodeMssqlApi(); + + let connectionURI: string = ''; + try { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: constants.connectionProgressTitle, + cancellable: false + }, async (_progress, _token) => { + // show progress bar while connecting to the users selected connection profile + connectionURI = await vscodeMssqlApi.connect(connectionInfo!); + } + ); + } catch (e) { + // mssql connection error will be shown to the user + return undefined; + } + + return connectionURI; +} + +export async function promptSelectTable(connectionURI: string, bindingType: BindingType, selectedDatabase: string): Promise { + const vscodeMssqlApi = await utils.getVscodeMssqlApi(); + const userObjectName = bindingType === BindingType.input ? constants.enterObjectName : constants.enterObjectNameToUpsert; + + // Create query to get list of tables from database selected + let tableQuery = tablesQuery(selectedDatabase); + const params = { ownerUri: connectionURI, queryString: tableQuery }; + // send SimpleExecuteRequest query to STS to get list of schema and tables based on the connection profile of the user + let queryResult: azureFunctionsContracts.SimpleExecuteResult = await vscodeMssqlApi.sendRequest(azureFunctionsContracts.SimpleExecuteRequest.type, params); + + // Get schema and table names from query result rows + const tableNames = queryResult.rows.map(r => r[0].displayValue); + // add manual entry option to table names list for user to choose from as well (with pencil icon) + let manuallyEnterObjectName = '$(pencil) ' + userObjectName; + tableNames.unshift(manuallyEnterObjectName); + // prompt user to select table from list of tables options + while (true) { + let selectedObject = await vscode.window.showQuickPick(tableNames, { + canPickMany: false, + title: constants.selectTable, + ignoreFocusOut: true + }); + + if (selectedObject === manuallyEnterObjectName) { + let selectedObject = promptToManuallyEnterObjectName(bindingType); + if (!selectedObject) { + // user cancelled so we will show the tables prompt again + continue; + } + } + + return selectedObject; + } +} + +export function tablesQuery(selectedDatabase: string): string { + let quotedDatabase = '[' + utils.escapeClosingBrackets(selectedDatabase) + ']'; + return `SELECT CONCAT(QUOTENAME(table_schema),'.',QUOTENAME(table_name)) from ${quotedDatabase}.INFORMATION_SCHEMA.TABLES where TABLE_TYPE = 'BASE TABLE'`; +} + +export async function promptToManuallyEnterObjectName(bindingType: BindingType): Promise { + // user manually enters table or view to query or upsert into + let selectedObject = await vscode.window.showInputBox({ + prompt: bindingType === BindingType.input ? constants.sqlTableOrViewToQuery : constants.sqlTableToUpsert, + placeHolder: constants.placeHolderObject, + validateInput: input => input ? undefined : constants.nameMustNotBeEmpty, + ignoreFocusOut: true + }); + return selectedObject; +} \ No newline at end of file diff --git a/extensions/sql-bindings/src/common/constants.ts b/extensions/sql-bindings/src/common/constants.ts index 6f632745a3..388802ba79 100644 --- a/extensions/sql-bindings/src/common/constants.ts +++ b/extensions/sql-bindings/src/common/constants.ts @@ -75,6 +75,9 @@ export const userPasswordLater = localize('userPasswordLater', 'In order to user export const openFile = localize('openFile', "Open File"); export const closeButton = localize('closeButton', "Close"); export const connectionProgressTitle = localize('connectionProgressTitle', "Testing SQL Server connection..."); +export const enterObjectName = localize('enterObjectName', 'Enter SQL table or view to query'); +export const enterObjectNameToUpsert = localize('enterObjectNameToUpsert', 'Enter SQL table to upsert into'); +export const selectTable = localize('selectTable', 'Select table to use'); export const selectConnectionError = (err?: any): string => err ? localize('selectConnectionError', "Failed to set connection string app setting: {0}", utils.getErrorMessage(err)) : localize('unableToSetConnectionString', "Failed to set connection string app setting"); export function selectBindingType(funcName?: string): string { return funcName ? localize('selectBindingTypeToSpecifiedFunction', "Select type of binding for the function '{0}'", funcName) : localize('selectBindingType', "Select type of binding"); } export function settingAlreadyExists(settingName: string): string { return localize('SettingAlreadyExists', 'Local app setting \'{0}\' already exists. Overwrite?', settingName); } diff --git a/extensions/sql-bindings/src/common/utils.ts b/extensions/sql-bindings/src/common/utils.ts index f06d99939a..38f1257178 100644 --- a/extensions/sql-bindings/src/common/utils.ts +++ b/extensions/sql-bindings/src/common/utils.ts @@ -173,4 +173,3 @@ export function getErrorType(error: any): string | undefined { return 'UnknownError'; } } - diff --git a/extensions/sql-bindings/src/contracts/azureFunctions/azureFunctionsContracts.ts b/extensions/sql-bindings/src/contracts/azureFunctions/azureFunctionsContracts.ts index 06272b8317..ce4fd8a2ce 100644 --- a/extensions/sql-bindings/src/contracts/azureFunctions/azureFunctionsContracts.ts +++ b/extensions/sql-bindings/src/contracts/azureFunctions/azureFunctionsContracts.ts @@ -19,3 +19,69 @@ export namespace AddSqlBindingRequest { export namespace GetAzureFunctionsRequest { export const type = new RequestType('azureFunctions/getAzureFunctions'); } + +// ------------------------------- < Execute String > ------------------------------------ + +// source: https://github.com/microsoft/azuredatastudio/blob/main/src/sql/azdata.d.ts#L1021 +export interface SimpleExecuteParams { + queryString: string; + ownerUri: string; +} + +/** + * Simple Query Execute Result will return rowCount, columnInfo, and rows from STS request + * source: https://github.com/microsoft/azuredatastudio/blob/main/src/sql/azdata.d.ts#L1026 + * rowCount is the number of rows returned with resultset + * columnInfo is the details about the columns that are povided as solutions + * rows is a 2D array of the cell values from the resultset + */ +export interface SimpleExecuteResult { + rowCount: number; + columnInfo: IDbColumn[]; + rows: DbCellValue[][]; +} + +// source: https://github.com/microsoft/sqlops-dataprotocolclient/blob/main/src/protocol.ts#L437 +export namespace SimpleExecuteRequest { + export const type = new RequestType('query/simpleexecute'); +} + +// source: https://github.com/microsoft/azuredatastudio/blob/main/src/sql/azdata.d.ts#L907 +export interface IDbColumn { + allowDBNull?: boolean | undefined; + baseCatalogName: string; + baseColumnName: string; + baseSchemaName: string; + baseServerName: string; + baseTableName: string; + columnName: string; + columnOrdinal?: number | undefined; + columnSize?: number | undefined; + isAliased?: boolean | undefined; + isAutoIncrement?: boolean | undefined; + isExpression?: boolean | undefined; + isHidden?: boolean | undefined; + isIdentity?: boolean | undefined; + isKey?: boolean | undefined; + isBytes?: boolean | undefined; + isChars?: boolean | undefined; + isSqlVariant?: boolean | undefined; + isUdt?: boolean | undefined; + dataType: string; + isXml?: boolean | undefined; + isJson?: boolean | undefined; + isLong?: boolean | undefined; + isReadOnly?: boolean | undefined; + isUnique?: boolean | undefined; + numericPrecision?: number | undefined; + numericScale?: number | undefined; + udtAssemblyQualifiedName: string; + dataTypeName: string; +} + +// source: https://github.com/microsoft/azuredatastudio/blob/main/src/sql/azdata.d.ts#L1066 +export interface DbCellValue { + displayValue: string; + isNull: boolean; + invariantCultureDisplayValue: string; +} \ No newline at end of file diff --git a/extensions/sql-bindings/src/dialogs/addSqlBindingQuickpick.ts b/extensions/sql-bindings/src/dialogs/addSqlBindingQuickpick.ts index 6c264c2571..045c4d8066 100644 --- a/extensions/sql-bindings/src/dialogs/addSqlBindingQuickpick.ts +++ b/extensions/sql-bindings/src/dialogs/addSqlBindingQuickpick.ts @@ -77,17 +77,7 @@ export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined): TelemetryReporter.createActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.getBindingType) .withAdditionalProperties(propertyBag).send(); - // 3. ask for object name for the binding - quickPickStep = 'getObjectName'; - const objectName = await azureFunctionsUtils.promptForObjectName(selectedBinding); - - if (!objectName) { - return; - } - TelemetryReporter.createActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.getObjectName) - .withAdditionalProperties(propertyBag).send(); - - // 4. ask for connection string setting name + // 3. ask for connection string setting name let projectUri: vscode.Uri | undefined; try { projectUri = await azureFunctionsUtils.getAFProjectContainingFile(uri); @@ -96,17 +86,26 @@ export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined): } quickPickStep = 'updateConnectionString'; - let connectionStringSettingName = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(projectUri); - if (!connectionStringSettingName) { + let connectionStringInfo = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(projectUri); + if (!connectionStringInfo) { return; } TelemetryReporter.createActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.updateConnectionString) .withAdditionalProperties(propertyBag).send(); + // 4. ask for object name for the binding + quickPickStep = 'getObjectName'; + const objectName = await azureFunctionsUtils.promptForObjectName(selectedBinding, connectionStringInfo.connectionInfo); + if (!objectName) { + return; + } + TelemetryReporter.createActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.getObjectName) + .withAdditionalProperties(propertyBag).send(); + // 5. insert binding try { quickPickStep = 'insertBinding'; - const result = await addSqlBinding(selectedBinding, uri.fsPath, azureFunctionName, objectName, connectionStringSettingName); + const result = await addSqlBinding(selectedBinding, uri.fsPath, azureFunctionName, objectName, connectionStringInfo.connectionStringSettingName!); if (!result.success) { void vscode.window.showErrorMessage(result.errorMessage); diff --git a/extensions/sql-bindings/src/extension.ts b/extensions/sql-bindings/src/extension.ts index 4401633380..4ae8759463 100644 --- a/extensions/sql-bindings/src/extension.ts +++ b/extensions/sql-bindings/src/extension.ts @@ -3,8 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { ITreeNodeInfo } from 'vscode-mssql'; -import { IExtension, BindingType, GetAzureFunctionsResult, ResultStatus } from 'sql-bindings'; +import { IConnectionInfo, ITreeNodeInfo } from 'vscode-mssql'; +import { IExtension, BindingType, GetAzureFunctionsResult, ResultStatus, IConnectionStringInfo } from 'sql-bindings'; import { addSqlBinding, createAzureFunction, getAzureFunctions } from './services/azureFunctionsService'; import { launchAddSqlBindingQuickpick } from './dialogs/addSqlBindingQuickpick'; import { promptForBindingType, promptAndUpdateConnectionStringSetting, promptForObjectName } from './common/azureFunctionsUtils'; @@ -23,11 +23,11 @@ export async function activate(context: vscode.ExtensionContext): Promise => { return promptForBindingType(funcName); }, - promptForObjectName: async (bindingType: BindingType): Promise => { - return promptForObjectName(bindingType); + promptForObjectName: async (bindingType: BindingType, connectionInfo?: IConnectionInfo): Promise => { + return promptForObjectName(bindingType, connectionInfo); }, - promptAndUpdateConnectionStringSetting: async (projectUri: vscode.Uri | undefined): Promise => { - return promptAndUpdateConnectionStringSetting(projectUri); + promptAndUpdateConnectionStringSetting: async (projectUri: vscode.Uri | undefined, connectionInfo?: IConnectionInfo): Promise => { + return promptAndUpdateConnectionStringSetting(projectUri, connectionInfo); }, getAzureFunctions: async (filePath: string): Promise => { return getAzureFunctions(filePath); diff --git a/extensions/sql-bindings/src/services/azureFunctionsService.ts b/extensions/sql-bindings/src/services/azureFunctionsService.ts index 644abcac3e..804ef289b8 100644 --- a/extensions/sql-bindings/src/services/azureFunctionsService.ts +++ b/extensions/sql-bindings/src/services/azureFunctionsService.ts @@ -11,7 +11,7 @@ import * as azureFunctionsUtils from '../common/azureFunctionsUtils'; import * as constants from '../common/constants'; import * as azureFunctionsContracts from '../contracts/azureFunctions/azureFunctionsContracts'; import { CreateAzureFunctionStep, TelemetryActions, TelemetryReporter, TelemetryViews, ExitReason } from '../common/telemetry'; -import { AddSqlBindingParams, BindingType, GetAzureFunctionsParams, GetAzureFunctionsResult, ResultStatus } from 'sql-bindings'; +import { AddSqlBindingParams, BindingType, GetAzureFunctionsParams, GetAzureFunctionsResult, IConnectionStringInfo, ResultStatus } from 'sql-bindings'; import { IConnectionInfo, ITreeNodeInfo } from 'vscode-mssql'; import { createAddConnectionStringStep } from '../createNewProject/addConnectionStringStep'; @@ -83,6 +83,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise { return; } projectFolder = projectFolders[0].fsPath; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) .withAdditionalProperties(propertyBag).send(); break; @@ -125,45 +126,13 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise { } TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) .withAdditionalProperties(propertyBag).withConnectionInfo(connectionInfo).send(); - telemetryStep = CreateAzureFunctionStep.getConnectionProfile; - let connectionURI: string = ''; - try { - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: constants.connectionProgressTitle, - cancellable: false - }, async (_progress, _token) => { - // list databases based on connection profile selected - connectionURI = await vscodeMssqlApi.connect(connectionInfo!); - } - ); - } catch (e) { - // mssql connection error will be shown to the user - // we will then prompt user to choose a connection profile again - continue; - } - // list databases based on connection profile selected - telemetryStep = CreateAzureFunctionStep.getDatabase; - let listDatabases = await vscodeMssqlApi.listDatabases(connectionURI); - const selectedDatabase = (await vscode.window.showQuickPick(listDatabases, { - canPickMany: false, - title: constants.selectDatabase, - ignoreFocusOut: true - })); - - if (!selectedDatabase) { - // User cancelled - // we will then prompt user to choose a connection profile again - continue; - } - connectionInfo.database = selectedDatabase; + telemetryStep = CreateAzureFunctionStep.getObjectName; // prompt user for object name to create function from - objectName = await azureFunctionsUtils.promptForObjectName(selectedBinding); + objectName = await azureFunctionsUtils.promptForObjectName(selectedBinding, connectionInfo); if (!objectName) { // user cancelled - return; + continue; } break; } @@ -213,16 +182,20 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise { let templateId: string = selectedBindingType === BindingType.input ? constants.inputTemplateID : constants.outputTemplateID; // prompt for Connection String Setting Name - let connectionStringSettingName: string | undefined = constants.sqlConnectionStringSetting; + let connectionStringInfo: IConnectionStringInfo | undefined = { connectionStringSettingName: constants.sqlConnectionStringSetting, connectionInfo: connectionInfo }; if (!isCreateNewProject && projectFile) { telemetryStep = CreateAzureFunctionStep.getConnectionStringSettingName; - connectionStringSettingName = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(vscode.Uri.parse(projectFile), connectionInfo); + connectionStringInfo = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(vscode.Uri.parse(projectFile), connectionInfo); + if (!connectionStringInfo) { + // User cancelled connection string setting name prompt or connection string method prompt + return; + } TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) .withAdditionalProperties(propertyBag) .withConnectionInfo(connectionInfo).send(); } // addtional execution step that will be used by vscode-azurefunctions to execute only when creating a new azure function project - let connectionStringExecuteStep = createAddConnectionStringStep(projectFolder, connectionInfo, connectionStringSettingName); + let connectionStringExecuteStep = createAddConnectionStringStep(projectFolder, connectionInfo, connectionStringInfo.connectionStringSettingName); // create C# Azure Function with SQL Binding telemetryStep = 'createFunctionAPI'; @@ -233,7 +206,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise { templateId: templateId, functionName: functionName, functionSettings: { - connectionStringSetting: connectionStringSettingName, + connectionStringSetting: connectionStringInfo.connectionStringSettingName, ...(selectedBindingType === BindingType.input && { object: objectName }), ...(selectedBindingType === BindingType.output && { table: objectName }) }, diff --git a/extensions/sql-bindings/src/sql-bindings.d.ts b/extensions/sql-bindings/src/sql-bindings.d.ts index 8140254bad..9218fc50b1 100644 --- a/extensions/sql-bindings/src/sql-bindings.d.ts +++ b/extensions/sql-bindings/src/sql-bindings.d.ts @@ -7,6 +7,7 @@ declare module 'sql-bindings' { import * as vscode from 'vscode'; + import { IConnectionInfo } from 'vscode-mssql'; export const enum extension { name = 'Microsoft.sql-bindings', @@ -36,14 +37,20 @@ declare module 'sql-bindings' { /** * Prompts the user to enter object name for the SQL query * @param bindingType Type of SQL Binding + * @param connectionInfo (optional) connection info from the selected connection profile + * if left undefined we prompt to manually enter the object name + * @returns the object name from user's input or menu choice */ - promptForObjectName(bindingType: BindingType): Promise; + promptForObjectName(bindingType: BindingType, connectionInfo?: IConnectionInfo): Promise; /** * Prompts the user to enter connection setting and updates it from AF project * @param projectUri Azure Function project uri - */ - promptAndUpdateConnectionStringSetting(projectUri: vscode.Uri | undefined): Promise; + * @param connectionInfo (optional) connection info from the user to update the connection string, + * if left undefined we prompt the user for the connection info + * @returns connection string setting name to be used for the createFunction API + */ + promptAndUpdateConnectionStringSetting(projectUri: vscode.Uri | undefined, connectionInfo?: IConnectionInfo): Promise; /** * Gets the names of the Azure Functions in the file @@ -119,4 +126,12 @@ declare module 'sql-bindings' { azureFunctions: string[]; } + /** + * Result from promptAndUpdateConnectionStringSetting + */ + export interface IConnectionStringInfo { + connectionStringSettingName: string; + connectionInfo: IConnectionInfo | undefined; + } + } diff --git a/extensions/sql-bindings/src/test/dialog/addSqlBindingQuickpick.test.ts b/extensions/sql-bindings/src/test/dialog/addSqlBindingQuickpick.test.ts index 115b5b009b..18d35ce3d6 100644 --- a/extensions/sql-bindings/src/test/dialog/addSqlBindingQuickpick.test.ts +++ b/extensions/sql-bindings/src/test/dialog/addSqlBindingQuickpick.test.ts @@ -7,14 +7,17 @@ import * as should from 'should'; import * as sinon from 'sinon'; import * as vscode from 'vscode'; import * as TypeMoq from 'typemoq'; +import * as fs from 'fs'; import * as utils from '../../common/utils'; import * as constants from '../../common/constants'; import * as azureFunctionUtils from '../../common/azureFunctionsUtils'; import * as azureFunctionService from '../../services/azureFunctionsService'; +import * as azureFunctionsContracts from '../../contracts/azureFunctions/azureFunctionsContracts'; import { createTestUtils, TestUtils, createTestCredentials } from '../testUtils'; import { launchAddSqlBindingQuickpick } from '../../dialogs/addSqlBindingQuickpick'; import { BindingType } from 'sql-bindings'; +import { IConnectionInfo } from 'vscode-mssql'; let testUtils: TestUtils; const fileUri = vscode.Uri.file('testUri'); @@ -46,6 +49,17 @@ describe('Add SQL Binding quick pick', () => { it('Should show error if adding SQL binding was not successful', async function (): Promise { sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); + let connectionCreds: IConnectionInfo = createTestCredentials();// Mocks promptForConnection + let connectionDetails = { options: connectionCreds }; + // set test vscode-mssql API calls + testUtils.vscodeMssqlIExtension.setup(x => x.promptForConnection(true)).returns(() => Promise.resolve(connectionCreds)); + testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); + testUtils.vscodeMssqlIExtension.setup(x => x.connect(connectionCreds)).returns(() => Promise.resolve('testConnectionURI')); + testUtils.vscodeMssqlIExtension.setup(x => x.listDatabases('testConnectionURI')).returns(() => Promise.resolve(['testDb'])); + const params = { ownerUri: 'testConnectionURI', queryString: azureFunctionUtils.tablesQuery('testDb') }; + testUtils.vscodeMssqlIExtension.setup(x => x.sendRequest(azureFunctionsContracts.SimpleExecuteRequest.type, params)) + .returns(() => Promise.resolve({ rowCount: 1, columnInfo: [], rows: [['[schema].[testTable]']] })); + sinon.stub(azureFunctionService, 'getAzureFunctions').withArgs(fileUri.fsPath).returns( Promise.resolve({ success: true, @@ -53,7 +67,7 @@ describe('Add SQL Binding quick pick', () => { azureFunctions: ['af1', 'af2'] })); //failure since no AFs are found in the project - sinon.stub(azureFunctionUtils, 'getAFProjectContainingFile').resolves(undefined); + const errormsg = 'Error inserting binding'; sinon.stub(azureFunctionService, 'addSqlBinding').withArgs( sinon.match.any, sinon.match.any, sinon.match.any, @@ -65,13 +79,22 @@ describe('Add SQL Binding quick pick', () => { const spy = sinon.spy(vscode.window, 'showErrorMessage'); // select Azure function - let quickpickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: 'af1' }); + let quickpickStub = sinon.stub(vscode.window, 'showQuickPick').returns(Promise.resolve('af1') as any); // select input or output binding quickpickStub.onSecondCall().resolves({ label: constants.input, type: BindingType.input }); - // give object name - let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('dbo.table1'); + sinon.stub(azureFunctionUtils, 'getAFProjectContainingFile').resolves(vscode.Uri.file('testUri')); + // select connection profile - create new + quickpickStub.onThirdCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); // give connection string setting name - inputBoxStub.onSecondCall().resolves('sqlConnectionString'); + sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('sqlConnectionString'); + quickpickStub.onCall(3).returns(Promise.resolve(constants.connectionProfile) as any); + quickpickStub.onCall(4).returns(Promise.resolve(constants.yesString) as any); + // setLocalAppSetting fails if we dont set writeFile stub + sinon.stub(fs.promises, 'writeFile'); + sinon.stub(azureFunctionUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'sqlConnectionString', 'testConnectionString').returns(Promise.resolve(true)); + sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package'); + quickpickStub.onCall(5).returns(Promise.resolve('testDb') as any); + quickpickStub.onCall(6).returns(Promise.resolve('[schema].[testTable]') as any); await launchAddSqlBindingQuickpick(vscode.Uri.file('testUri')); diff --git a/extensions/sql-bindings/src/test/testUtils.ts b/extensions/sql-bindings/src/test/testUtils.ts index d85909ef43..cb294f6e53 100644 --- a/extensions/sql-bindings/src/test/testUtils.ts +++ b/extensions/sql-bindings/src/test/testUtils.ts @@ -79,7 +79,7 @@ export function createTestCredentials(): vscodeMssql.IConnectionInfo { accountId: 'test-account-id', tenantId: 'test-tenant-id', port: 1234, - authenticationType: 'test', + authenticationType: 'SqlLogin', azureAccountToken: '', expiresOn: 0, encrypt: false,