diff --git a/extensions/sql-bindings/package.json b/extensions/sql-bindings/package.json index 5fa6f59be8..5174f25d23 100644 --- a/extensions/sql-bindings/package.json +++ b/extensions/sql-bindings/package.json @@ -56,7 +56,7 @@ "view/item/context": [ { "command": "sqlBindings.createAzureFunction", - "when": "view == objectExplorer && viewItem == Table", + "when": "view == objectExplorer && (viewItem == Table || viewItem == View)", "group": "zAzure_Function@1" } ] diff --git a/extensions/sql-bindings/src/common/azureFunctionsUtils.ts b/extensions/sql-bindings/src/common/azureFunctionsUtils.ts index 5edf8ca23c..593b044668 100644 --- a/extensions/sql-bindings/src/common/azureFunctionsUtils.ts +++ b/extensions/sql-bindings/src/common/azureFunctionsUtils.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import * as utils from './utils'; import * as constants from './constants'; import * as azureFunctionsContracts from '../contracts/azureFunctions/azureFunctionsContracts'; -import { BindingType, IConnectionStringInfo } from 'sql-bindings'; +import { BindingType, IConnectionStringInfo, ObjectType } 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'; @@ -286,9 +286,13 @@ export async function isFunctionProject(folderPath: string): Promise { /** * Prompts the user to select type of binding and returns result + * @param objectType (Optional) The type of object user choose to insert/upsert into * @param funcName (Optional) Name of the function to which we are adding the SQL Binding + * @returns binding type or undefined if the user cancelled out of the prompt */ -export async function promptForBindingType(funcName?: string): Promise { +export async function promptForBindingType(objectType?: ObjectType, funcName?: string): Promise { + // check to see if objectType is view + let isView = (objectType === ObjectType.View); const inputOutputItems: (vscode.QuickPickItem & { type: BindingType })[] = [ { label: constants.input, @@ -302,7 +306,8 @@ export async function promptForBindingType(funcName?: string): Promise { + const objectTypes: (vscode.QuickPickItem & { type: ObjectType })[] = + [{ label: constants.table, type: ObjectType.Table }, { label: constants.view, type: ObjectType.View }]; + const selectedObjectType = (await vscode.window.showQuickPick(objectTypes, { + canPickMany: false, + title: constants.selectSqlTableOrViewPrompt, + ignoreFocusOut: true + })); + + return selectedObjectType?.type; +} + /** * 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 + * @param objectType (optional) type of object to query/upsert into * @returns the object name from user's input or menu choice */ -export async function promptForObjectName(bindingType: BindingType, connectionInfo?: IConnectionInfo): Promise { +export async function promptForObjectName(bindingType: BindingType, connectionInfo?: IConnectionInfo, objectType?: ObjectType): Promise { // show the connection string methods (user input and connection profile options) let connectionURI: string | undefined; let selectedDatabase: string | undefined; @@ -329,9 +350,8 @@ export async function promptForObjectName(bindingType: BindingType, connectionIn 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 + // Prompt user to select a table/view based on connection profile and selected database + // get connectionURI and selectedDatabase to be used for listing tables/view query request connectionURI = await getConnectionURI(connectionInfo); if (!connectionURI) { // mssql connection error @@ -345,7 +365,7 @@ export async function promptForObjectName(bindingType: BindingType, connectionIn connectionInfo.database = selectedDatabase; - let selectedObjectName = await promptSelectTable(connectionURI, bindingType, selectedDatabase); + let selectedObjectName = await promptSelectObject(connectionURI, bindingType, selectedDatabase, objectType); return selectedObjectName; } @@ -618,35 +638,38 @@ export async function getConnectionURI(connectionInfo: IConnectionInfo): Promise return connectionURI; } -export async function promptSelectTable(connectionURI: string, bindingType: BindingType, selectedDatabase: string): Promise { +export async function promptSelectObject(connectionURI: string, bindingType: BindingType, selectedDatabase: string, objectType?: string): Promise { const vscodeMssqlApi = await utils.getVscodeMssqlApi(); - const userObjectName = bindingType === BindingType.input ? constants.enterObjectName : constants.enterObjectNameToUpsert; + let isView = (objectType === ObjectType.View); - // Create query to get list of tables from database selected - let tableQuery = tablesQuery(selectedDatabase); - const params = { ownerUri: connectionURI, queryString: tableQuery }; + const userObjectName = isView ? constants.enterViewName : bindingType === BindingType.input ? constants.enterTableName : constants.enterTableNameToUpsert; + + // Create query to get list of tables or views from selected database + let listQuery = isView ? viewsQuery(selectedDatabase) : tablesQuery(selectedDatabase); + const params = { ownerUri: connectionURI, queryString: listQuery }; let queryResult: azureFunctionsContracts.SimpleExecuteResult | undefined; + // send SimpleExecuteRequest query to STS to get list of schema and tables based on the connection profile and database of the user await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, - title: constants.tableListProgressTitle, + title: isView ? constants.viewListProgressTitle : constants.tableListProgressTitle, cancellable: false }, async (_progress, _token) => { queryResult = 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) + // Get schema and table/view names from query result rows + const objectNames = queryResult!.rows.map(r => r[0].displayValue); + // add manual entry option to object names list for user to choose from as well (with pencil icon) let manuallyEnterObjectName = constants.manuallyEnterObjectName(userObjectName); - tableNames.unshift(manuallyEnterObjectName); - // prompt user to select table from list of tables options + objectNames.unshift(manuallyEnterObjectName); + // prompt user to select object from list of objects while (true) { - let selectedObject = await vscode.window.showQuickPick(tableNames, { + let selectedObject = await vscode.window.showQuickPick(objectNames, { canPickMany: false, - title: constants.selectTable, + title: isView ? constants.selectView : constants.selectTable, ignoreFocusOut: true }); @@ -667,10 +690,15 @@ export function tablesQuery(selectedDatabase: string): string { return `SELECT CONCAT(QUOTENAME(table_schema),'.',QUOTENAME(table_name)) from ${quotedDatabase}.INFORMATION_SCHEMA.TABLES where TABLE_TYPE = 'BASE TABLE'`; } +export function viewsQuery(selectedDatabase: string): string { + let quotedDatabase = '[' + utils.escapeClosingBrackets(selectedDatabase) + ']'; + return `SELECT CONCAT(QUOTENAME(table_schema),'.',QUOTENAME(table_name)) from ${quotedDatabase}.INFORMATION_SCHEMA.VIEWS;`; +} + 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, + prompt: bindingType === BindingType.input ? constants.sqlObjectToQuery : constants.sqlTableToUpsert, placeHolder: constants.placeHolderObject, validateInput: input => input ? undefined : constants.nameMustNotBeEmpty, ignoreFocusOut: true diff --git a/extensions/sql-bindings/src/common/constants.ts b/extensions/sql-bindings/src/common/constants.ts index a2e91754bb..5d82a8de0a 100644 --- a/extensions/sql-bindings/src/common/constants.ts +++ b/extensions/sql-bindings/src/common/constants.ts @@ -48,7 +48,7 @@ export const output = localize('output', "Output"); export const inputDescription = localize('inputDescription', "Retrieves data from a database"); export const outputDescription = localize('outputDescription', "Save data to a database"); export const selectAzureFunction = localize('selectAzureFunction', "Select an Azure function in the current file to add SQL binding to"); -export const sqlTableOrViewToQuery = localize('sqlTableOrViewToQuery', "SQL table or view to query"); +export const sqlObjectToQuery = localize('sqlObjectToQuery', "SQL object to query"); export const sqlTableToUpsert = localize('sqlTableToUpsert', "SQL table to upsert into"); export const connectionStringSetting = localize('connectionStringSetting', "Connection string setting name"); export const selectSetting = localize('selectSetting', "Select SQL connection string setting from local.settings.json"); @@ -74,10 +74,16 @@ export const openFile = localize('openFile', "Open File"); export const closeButton = localize('closeButton', "Close"); export const enterPasswordPrompt = localize('enterPasswordPrompt', '(Optional) Enter connection password to save in local.settings.json'); 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 enterTableName = localize('enterTableName', 'Enter SQL table to query'); +export const enterViewName = localize('enterViewName', 'Enter SQL view to query'); +export const enterTableNameToUpsert = localize('enterTableNameToUpsert', 'Enter SQL table to upsert into'); export const selectTable = localize('selectTable', 'Select table to use'); +export const selectView = localize('selectView', 'Select view to use'); export const tableListProgressTitle = localize('tableListProgressTitle', "Fetching tables for selected database..."); +export const viewListProgressTitle = localize('viewListProgressTitle', "Fetching views for selected database..."); +export const table = localize('table', 'Table'); +export const view = localize('view', 'View'); +export const selectSqlTableOrViewPrompt = localize('selectSqlTableOrViewPrompt', "Select object type to insert or upsert into"); export const failedToSetSetting = (err?: any): string => err ? localize('failedToSetSetting', "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/telemetry.ts b/extensions/sql-bindings/src/common/telemetry.ts index 96ae750cdd..4758211788 100644 --- a/extensions/sql-bindings/src/common/telemetry.ts +++ b/extensions/sql-bindings/src/common/telemetry.ts @@ -37,7 +37,7 @@ export enum CreateAzureFunctionStep { getSelectedFolder = 'getSelectedFolder', getBindingType = 'getBindingType', launchFromCommandPalette = 'launchFromCommandPalette', - launchFromTable = 'launchFromTable', + launchFromObjectExplorer = 'launchFromObjectExplorer', getConnectionProfile = 'getConnectionProfile', getDatabase = 'getDatabase', getObjectName = 'getObjectName', @@ -54,4 +54,4 @@ export enum ExitReason { timeout = 'timeout', error = 'error', exit = 'exit' -} \ No newline at end of file +} diff --git a/extensions/sql-bindings/src/extension.ts b/extensions/sql-bindings/src/extension.ts index 29a554aff2..a251b380bc 100644 --- a/extensions/sql-bindings/src/extension.ts +++ b/extensions/sql-bindings/src/extension.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import { IConnectionInfo, ITreeNodeInfo } from 'vscode-mssql'; -import { IExtension, BindingType, GetAzureFunctionsResult, ResultStatus, IConnectionStringInfo } from 'sql-bindings'; +import { IExtension, BindingType, GetAzureFunctionsResult, ResultStatus, IConnectionStringInfo, ObjectType } from 'sql-bindings'; import { addSqlBinding, createAzureFunction, getAzureFunctions } from './services/azureFunctionsService'; import { launchAddSqlBindingQuickpick } from './dialogs/addSqlBindingQuickpick'; import { promptForBindingType, promptAndUpdateConnectionStringSetting, promptForObjectName, addSqlNugetReferenceToProjectFile } from './common/azureFunctionsUtils'; @@ -23,11 +23,11 @@ export async function activate(context: vscode.ExtensionContext): Promise => { return createAzureFunction(); }, - promptForBindingType: async (funcName?: string): Promise => { - return promptForBindingType(funcName); + promptForBindingType: async (objectType?: ObjectType, funcName?: string): Promise => { + return promptForBindingType(objectType, funcName); }, - promptForObjectName: async (bindingType: BindingType, connectionInfo?: IConnectionInfo): Promise => { - return promptForObjectName(bindingType, connectionInfo); + promptForObjectName: async (bindingType: BindingType, connectionInfo?: IConnectionInfo, objectType?: ObjectType): Promise => { + return promptForObjectName(bindingType, connectionInfo, objectType); }, promptAndUpdateConnectionStringSetting: async (projectUri: vscode.Uri | undefined, connectionInfo?: IConnectionInfo): Promise => { return promptAndUpdateConnectionStringSetting(projectUri, connectionInfo); diff --git a/extensions/sql-bindings/src/services/azureFunctionsService.ts b/extensions/sql-bindings/src/services/azureFunctionsService.ts index 758459f0ad..f817704e65 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, IConnectionStringInfo, ResultStatus } from 'sql-bindings'; +import { AddSqlBindingParams, BindingType, GetAzureFunctionsParams, GetAzureFunctionsResult, IConnectionStringInfo, ObjectType, ResultStatus } from 'sql-bindings'; import { IConnectionInfo, ITreeNodeInfo } from 'vscode-mssql'; import { createAddConnectionStringStep } from '../createNewProject/addConnectionStringStep'; @@ -99,23 +99,32 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise { // create a system file watcher for the project folder newFunctionFileObject = azureFunctionsUtils.waitForNewFunctionFile(projectFolder); - // Prompt user for binding type - telemetryStep = CreateAzureFunctionStep.getBindingType; - let selectedBindingType: BindingType | undefined; - let selectedBinding = await azureFunctionsUtils.promptForBindingType(); - if (!selectedBinding) { - return; - } - selectedBindingType = selectedBinding; - propertyBag.bindingType = selectedBindingType; - TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) - .withAdditionalProperties(propertyBag).send(); - // Get connection string parameters and construct object name from prompt or connectionInfo given let objectName: string | undefined; + let selectedBindingType: BindingType | undefined; if (!node) { - // if user selects command in command palette we prompt user for information + // user selects command in command palette we prompt user for information telemetryStep = CreateAzureFunctionStep.launchFromCommandPalette; + + let chosenObjectType = await azureFunctionsUtils.promptForObjectType(); + if (!chosenObjectType) { + // User cancelled + return; + } + + // Prompt user for binding type + telemetryStep = CreateAzureFunctionStep.getBindingType; + selectedBindingType = await azureFunctionsUtils.promptForBindingType(chosenObjectType); + if (!selectedBindingType) { + return; + } + + // send telemetry for chosen object type and binding type + propertyBag.objectType = chosenObjectType; + propertyBag.bindingType = selectedBindingType; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) + .withAdditionalProperties(propertyBag).send(); + // prompt user for connection profile to get connection info while (true) { try { @@ -135,7 +144,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise { telemetryStep = CreateAzureFunctionStep.getObjectName; // prompt user for object name to create function from - objectName = await azureFunctionsUtils.promptForObjectName(selectedBinding, connectionInfo); + objectName = await azureFunctionsUtils.promptForObjectName(selectedBindingType, connectionInfo, chosenObjectType); if (!objectName) { // user cancelled continue; @@ -143,12 +152,12 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise { break; } } else { - // if user selects table in tree view we use connection info from Object Explorer node - telemetryStep = CreateAzureFunctionStep.launchFromTable; + // user selects table in tree view we use connection info from Object Explorer node + telemetryStep = CreateAzureFunctionStep.launchFromObjectExplorer; connectionInfo = node.connectionInfo; TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) .withAdditionalProperties(propertyBag).withConnectionInfo(connectionInfo).send(); - // set the database containing the selected table so it can be used + // set the database containing the selected table or view so it can be used // for the initial catalog property of the connection string let newNode: ITreeNodeInfo = node; while (newNode) { @@ -159,6 +168,21 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise { newNode = newNode.parentNode; } } + + // Prompt user for binding type + telemetryStep = CreateAzureFunctionStep.getBindingType; + let nodeType = ObjectType.Table === node.nodeType ? ObjectType.Table : ObjectType.View; + selectedBindingType = await azureFunctionsUtils.promptForBindingType(nodeType); + if (!selectedBindingType) { + return; + } + + // send telemetry for object type and binding type + propertyBag.objectType = node.nodeType; + propertyBag.bindingType = selectedBindingType; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) + .withAdditionalProperties(propertyBag).send(); + objectName = utils.generateQuotedFullName(node.metadata.schema, node.metadata.name); TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) .withAdditionalProperties(propertyBag).withConnectionInfo(connectionInfo).send(); diff --git a/extensions/sql-bindings/src/sql-bindings.d.ts b/extensions/sql-bindings/src/sql-bindings.d.ts index c6b44a391b..801f700756 100644 --- a/extensions/sql-bindings/src/sql-bindings.d.ts +++ b/extensions/sql-bindings/src/sql-bindings.d.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - declare module 'sql-bindings' { import * as vscode from 'vscode'; @@ -14,6 +13,11 @@ declare module 'sql-bindings' { vsCodeName = 'ms-mssql.sql-bindings-vscode' } + export const enum ObjectType { + Table = 'Table', + View = 'View' + } + /** * sql bindings extension */ @@ -34,19 +38,24 @@ declare module 'sql-bindings' { createAzureFunction(): Promise; /** - * Prompts the user to select type of binding and returns result or undefined if the user cancelled out of the prompt - * @param funcName (Optional) Name of the function we are adding the SQL Binding to + * Prompts the user to select type of binding and returns result + * @param objectType (Optional) The type of object user choose to insert/upsert into + * if left undefined we prompt user to choose between input or output binding types + * @param funcName (Optional) Name of the function to which we are adding the SQL Binding + * @returns binding type or undefined if the user cancelled out of the prompt */ - promptForBindingType(funcName?: string): Promise; + promptForBindingType(objectType?: ObjectType, funcName?: string): Promise; /** * 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 + * @param objectType (optional) type of object to query/upsert into + * if left undefined we prompt user to select table to use or manually enter object name * @returns the object name from user's input or menu choice */ - promptForObjectName(bindingType: BindingType, connectionInfo?: IConnectionInfo): Promise; + promptForObjectName(bindingType: BindingType, connectionInfo?: IConnectionInfo, objectType?: ObjectType): Promise; /** * Prompts the user to enter connection setting and updates it from AF project @@ -168,5 +177,4 @@ declare module 'sql-bindings' { connectionStringSettingName: string; connectionInfo: IConnectionInfo | undefined; } - } diff --git a/extensions/sql-bindings/src/test/service/azureFunctionsService.test.ts b/extensions/sql-bindings/src/test/service/azureFunctionsService.test.ts index cfeed2d0cf..65e4840db3 100644 --- a/extensions/sql-bindings/src/test/service/azureFunctionsService.test.ts +++ b/extensions/sql-bindings/src/test/service/azureFunctionsService.test.ts @@ -15,8 +15,8 @@ import * as utils from '../../common/utils'; import * as azureFunctionsContracts from '../../contracts/azureFunctions/azureFunctionsContracts'; import * as azureFunctionService from '../../services/azureFunctionsService'; -import { BindingType } from 'sql-bindings'; -import { IConnectionInfo } from 'vscode-mssql'; +import { BindingType, ObjectType } from 'sql-bindings'; +import { ConnectionDetails, IConnectionInfo } from 'vscode-mssql'; import { createTestCredentials, createTestTableNode, createTestUtils, TestUtils } from '../testUtils'; const rootFolderPath = 'test'; @@ -36,118 +36,6 @@ describe('AzureFunctionsService', () => { constants.install, constants.learnMore, constants.doNotInstall]); }); - it('Should create azure function project using the command from command palette (no connection info)', async function (): Promise { - // This test will have an azure function project already in the project and the azure functions extension installed (stubbed) - sinon.stub(azureFunctionUtils, 'getAzureFunctionsExtensionApi').resolves(testUtils.azureFunctionsExtensionApi.object); // set azure functions extension api - sinon.stub(azureFunctionUtils, 'getAzureFunctionProject').resolves(projectFilePath); //set azure function project to have one project - sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); - - // create fake connection string settings for local.setting.json to be used - sinon.stub(fs.promises, 'access').onFirstCall().resolves(); - sinon.stub(fs, 'readFileSync').withArgs(sinon.match.any).returns( - `{"IsEncrypted": false, - "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` - ); - - let connectionInfo: IConnectionInfo = createTestCredentials(); // create test connectionInfo - - let connectionDetails = { options: connectionInfo }; - testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); - - const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); - - // select input or output binding - let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.input, type: BindingType.input }); - - // no table used for connection info so prompt user to get connection info - testUtils.vscodeMssqlIExtension.setup(x => x.promptForConnection(true)).returns(() => Promise.resolve(connectionInfo)); - testUtils.vscodeMssqlIExtension.setup(x => x.connect(connectionInfo)).returns(() => Promise.resolve('testConnectionURI')); - testUtils.vscodeMssqlIExtension.setup(x => x.listDatabases('testConnectionURI')).returns(() => Promise.resolve(['testDb'])); - // select the testDB from list of databases based on connection info - quickPickStub.onSecondCall().resolves(('testDb') as any); - // get tables from selected database - 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: [[{ displayValue: '[schema].[testTable]' }]] })); - // select the schema.testTable from list of tables based on connection info and database - quickPickStub.onThirdCall().resolves(('[schema].[testTable]') as any); - - // set azure function name - let inputStub = sinon.stub(vscode.window, 'showInputBox').resolves('testFunctionName'); - - // promptAndUpdateConnectionStringSetting - quickPickStub.onCall(3).resolves({ label: constants.createNewLocalAppSettingWithIcon }); - inputStub.onSecondCall().resolves('SqlConnectionString'); - // promptConnectionStringPasswordAndUpdateConnectionString - tested in AzureFunctionUtils.test.ts - quickPickStub.onCall(4).resolves((constants.yesString) as any); - testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); - // setLocalAppSetting with connection string setting name and connection string - // fails if we dont set writeFile stub - sinon.stub(fs.promises, 'writeFile').resolves(); - sinon.stub(azureFunctionUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'SqlConnectionString', 'testConnectionString').resolves((true)); - sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package'); - - const testWatcher = TypeMoq.Mock.ofType().object; - sinon.stub(azureFunctionUtils, 'waitForNewFunctionFile').withArgs(sinon.match.any).returns({ filePromise: Promise.resolve('TestFileCreated'), watcherDisposable: testWatcher }); - - should(connectionInfo.database).equal('my_db', 'ConnectionInfo database should not be changed'); - await azureFunctionService.createAzureFunction(); - - should(showErrorMessageSpy.notCalled).be.true('showErrorMessage should not have been called'); - // set the connection info to be the one the user selects from list of databases quickpick - should(connectionInfo.database).equal('testDb', 'connectionInfo.database should be testDb after user selects testDb'); - }); - - it('Should create azure function project using command via the sql server table OE', async function (): Promise { - // This test will have an azure function project already in the project and the azure functions extension installed (stubbed) - sinon.stub(azureFunctionUtils, 'getAzureFunctionsExtensionApi').resolves(testUtils.azureFunctionsExtensionApi.object); // set azure functions extension api - sinon.stub(azureFunctionUtils, 'getAzureFunctionProject').resolves(projectFilePath); //set azure function project to have one project - sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); - - // create fake connection string settings for local.setting.json to be used - sinon.stub(fs.promises, 'access').onFirstCall().resolves(); - sinon.stub(fs, 'readFileSync').withArgs(sinon.match.any).returns( - `{"IsEncrypted": false, - "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` - ); - - let connectionInfo: IConnectionInfo = createTestCredentials();// create test connectionInfo - let connectionDetails = { options: connectionInfo }; - testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); - - const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); - - // select input or output binding - let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.input, type: BindingType.input }); - // table node used when creating azure function project - let tableTestNode = createTestTableNode(connectionInfo); - - // set azure function name - let inputStub = sinon.stub(vscode.window, 'showInputBox').resolves('testFunctionName'); - - // promptAndUpdateConnectionStringSetting - quickPickStub.onSecondCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); - inputStub.onSecondCall().resolves('SqlConnectionString'); - // promptConnectionStringPasswordAndUpdateConnectionString - tested in AzureFunctionUtils.test.ts - quickPickStub.onThirdCall().resolves((constants.yesString) as any); - testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); - // setLocalAppSetting with connection string setting name and connection string - // fails if we dont set writeFile stub - sinon.stub(fs.promises, 'writeFile').resolves(); - sinon.stub(azureFunctionUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'SqlConnectionString', 'testConnectionString').resolves((true)); - sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package'); - - const testWatcher = TypeMoq.Mock.ofType().object; - sinon.stub(azureFunctionUtils, 'waitForNewFunctionFile').withArgs(sinon.match.any).returns({ filePromise: Promise.resolve('TestFileCreated'), watcherDisposable: testWatcher }); - - should(connectionInfo.database).equal('my_db', 'ConnectionInfo database should not be changed'); - await azureFunctionService.createAzureFunction(tableTestNode); - - should(showErrorMessageSpy.notCalled).be.true('showErrorMessage should not have been called'); - // set the connection info to be the one used from the test table node from OE - should(connectionInfo.database).equal('testDb', 'connectionInfo.database should be testDb after user selects testDb'); - }); - it('Should open link to learn more about SQL bindings when no azure function project found in folder or workspace', async function (): Promise { // This test will ask user that an azure function project must be opened to create an azure function with sql binding sinon.stub(azureFunctionUtils, 'getAzureFunctionsExtensionApi').resolves(testUtils.azureFunctionsExtensionApi.object); // set azure functions extension api @@ -205,11 +93,199 @@ describe('AzureFunctionsService', () => { const testWatcher = TypeMoq.Mock.ofType().object; sinon.stub(azureFunctionUtils, 'waitForNewFunctionFile').withArgs(sinon.match.any).returns({ filePromise: Promise.resolve('TestFileCreated'), watcherDisposable: testWatcher }); - should(connectionInfo.database).equal('my_db', 'ConnectionInfo database should not be changed'); + should(connectionInfo.database).equal('my_db', 'Initial ConnectionInfo database should be my_db'); await azureFunctionService.createAzureFunction(tableTestNode); should(showErrorStub.calledOnce).be.true('showErrorMessage should have been called'); }); + + describe('For table object', () => { + let connectionInfo: IConnectionInfo; + let connectionDetails: ConnectionDetails; + let showErrorMessageSpy: sinon.SinonSpy; + + beforeEach(function (): void { + // This test will have an azure function project already in the project and the azure functions extension installed (stubbed) + sinon.stub(azureFunctionUtils, 'getAzureFunctionsExtensionApi').resolves(testUtils.azureFunctionsExtensionApi.object); // set azure functions extension api + sinon.stub(azureFunctionUtils, 'getAzureFunctionProject').resolves(projectFilePath); //set azure function project to have one project + sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); + + // create fake connection string settings for local.setting.json to be used + sinon.stub(fs.promises, 'access').onFirstCall().resolves(); + sinon.stub(fs, 'readFileSync').withArgs(sinon.match.any).returns( + `{"IsEncrypted": false, + "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` + ); + + connectionInfo = createTestCredentials(); // create test connectionInfo + connectionDetails = { options: connectionInfo }; // use for getConnectionString request + testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); + + // setLocalAppSetting with connection string setting name and connection string + // fails if we dont set writeFile stub + sinon.stub(fs.promises, 'writeFile').resolves(); + sinon.stub(azureFunctionUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'SqlConnectionString', 'testConnectionString').resolves((true)); + sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package'); + + const testWatcher = TypeMoq.Mock.ofType().object; + sinon.stub(azureFunctionUtils, 'waitForNewFunctionFile').withArgs(sinon.match.any).returns({ filePromise: Promise.resolve('TestFileCreated'), watcherDisposable: testWatcher }); + + showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); // error message spy to be used for checking tests + }); + + it('Should create azure function project using the command from command palette (no connection info)', async function (): Promise { + // select table + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: constants.input, type: ObjectType.Table } as any); + // select input or output binding + quickPickStub.onSecondCall().resolves({ label: constants.input, type: BindingType.input }); + + // no connection node used for connection info so prompt user to get connection info + testUtils.vscodeMssqlIExtension.setup(x => x.promptForConnection(true)).returns(() => Promise.resolve(connectionInfo)); + testUtils.vscodeMssqlIExtension.setup(x => x.connect(connectionInfo)).returns(() => Promise.resolve('testConnectionURI')); + testUtils.vscodeMssqlIExtension.setup(x => x.listDatabases('testConnectionURI')).returns(() => Promise.resolve(['testDb'])); + // select the testDB from list of databases based on connection info + quickPickStub.onThirdCall().resolves(('testDb') as any); + // get tables from selected database + 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: [[{ displayValue: '[schema].[testTable]' }]] })); + // select the schema.testTable from list of tables based on connection info and database + quickPickStub.onCall(3).resolves(('[schema].[testTable]') as any); + + // set azure function name + let inputStub = sinon.stub(vscode.window, 'showInputBox').resolves('testFunctionName'); + + // promptAndUpdateConnectionStringSetting + quickPickStub.onCall(4).resolves({ label: constants.createNewLocalAppSettingWithIcon }); + inputStub.onSecondCall().resolves('SqlConnectionString'); + // promptConnectionStringPasswordAndUpdateConnectionString - tested in AzureFunctionUtils.test.ts + quickPickStub.onCall(5).resolves((constants.yesString) as any); + testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); + + should(connectionInfo.database).equal('my_db', 'Initial ConnectionInfo database should be my_db'); + await azureFunctionService.createAzureFunction(); + + should(showErrorMessageSpy.notCalled).be.true('showErrorMessage should not have been called'); + should(connectionInfo.database).equal('testDb', 'connectionInfo.database should be testDb after user selects testDb'); + }); + + it('Should create azure function project using command via the sql server table object explorer', async function (): Promise { + // select input or output binding + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.input, type: BindingType.input }); + // table node used when creating azure function project + let tableTestNode = createTestTableNode(connectionInfo); + + // set azure function name + let inputStub = sinon.stub(vscode.window, 'showInputBox').resolves('testFunctionName'); + + // promptAndUpdateConnectionStringSetting + quickPickStub.onSecondCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); + inputStub.onSecondCall().resolves('SqlConnectionString'); + // promptConnectionStringPasswordAndUpdateConnectionString - tested in AzureFunctionUtils.test.ts + quickPickStub.onThirdCall().resolves((constants.yesString) as any); + testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); + + should(connectionInfo.database).equal('my_db', 'Initial ConnectionInfo database should be my_db'); + await azureFunctionService.createAzureFunction(tableTestNode); + + should(showErrorMessageSpy.notCalled).be.true('showErrorMessage should not have been called'); + should(connectionInfo.database).equal('testDb', 'connectionInfo.database should be testDb used from the test table node from OE'); + }); + }); + + describe('For view object', function (): void { + let connectionInfo: IConnectionInfo; + let connectionDetails: ConnectionDetails; + let showErrorMessageSpy: sinon.SinonSpy; + + beforeEach(function (): void { + // This test will have an azure function project already in the project and the azure functions extension installed (stubbed) + sinon.stub(azureFunctionUtils, 'getAzureFunctionsExtensionApi').resolves(testUtils.azureFunctionsExtensionApi.object); // set azure functions extension api + sinon.stub(azureFunctionUtils, 'getAzureFunctionProject').resolves(projectFilePath); //set azure function project to have one project + sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); + + // create fake connection string settings for local.setting.json to be used + sinon.stub(fs.promises, 'access').onFirstCall().resolves(); + sinon.stub(fs, 'readFileSync').withArgs(sinon.match.any).returns( + `{"IsEncrypted": false, + "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` + ); + + connectionInfo = createTestCredentials(); // create test connectionInfo + connectionDetails = { options: connectionInfo }; // use for getConnectionString request + testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); + + // setLocalAppSetting with connection string setting name and connection string + // fails if we dont set writeFile stub + sinon.stub(fs.promises, 'writeFile').resolves(); + sinon.stub(azureFunctionUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'SqlConnectionString', 'testConnectionString').resolves((true)); + sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package'); + + const testWatcher = TypeMoq.Mock.ofType().object; + sinon.stub(azureFunctionUtils, 'waitForNewFunctionFile').withArgs(sinon.match.any).returns({ filePromise: Promise.resolve('TestFileCreated'), watcherDisposable: testWatcher }); + + showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); // error message spy to be used for checking tests + }); + + it('Should create azure function project using the command from command palette (no connection info)', async function (): Promise { + // select view + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: constants.input, type: ObjectType.View } as any); + // input binding should be used for views + + // no connection node used for connection info so prompt user to get connection info + testUtils.vscodeMssqlIExtension.setup(x => x.promptForConnection(true)).returns(() => Promise.resolve(connectionInfo)); + testUtils.vscodeMssqlIExtension.setup(x => x.connect(connectionInfo)).returns(() => Promise.resolve('testConnectionURI')); + testUtils.vscodeMssqlIExtension.setup(x => x.listDatabases('testConnectionURI')).returns(() => Promise.resolve(['testDb'])); + // select the testDB from list of databases based on connection info + quickPickStub.onSecondCall().resolves(('testDb') as any); + // get views from selected database + const params = { ownerUri: 'testConnectionURI', queryString: azureFunctionUtils.viewsQuery('testDb') }; + testUtils.vscodeMssqlIExtension.setup(x => x.sendRequest(azureFunctionsContracts.SimpleExecuteRequest.type, params)) + .returns(() => Promise.resolve({ rowCount: 1, columnInfo: [], rows: [[{ displayValue: '[schema].[testView]' }]] })); + // select the schema.testView from list of tables based on connection info and database + quickPickStub.onThirdCall().resolves(('[schema].[testView]') as any); + + // set azure function name + let inputStub = sinon.stub(vscode.window, 'showInputBox').resolves('testFunctionName'); + + // promptAndUpdateConnectionStringSetting + quickPickStub.onCall(3).resolves({ label: constants.createNewLocalAppSettingWithIcon }); + inputStub.onSecondCall().resolves('SqlConnectionString'); + // promptConnectionStringPasswordAndUpdateConnectionString - tested in AzureFunctionUtils.test.ts + quickPickStub.onCall(4).resolves((constants.yesString) as any); + testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); + + should(connectionInfo.database).equal('my_db', 'Initial ConnectionInfo database should be my_db'); + await azureFunctionService.createAzureFunction(); + + should(showErrorMessageSpy.notCalled).be.true('showErrorMessage should not have been called'); + should(connectionInfo.database).equal('testDb', 'connectionInfo.database should be testDb after user selects testDb'); + }); + + it('Should create azure function project using command via the sql server view object explorer', async function (): Promise { + // Since views is stricly used only with input binding we go directly to asking for function name + // set azure function name + let inputStub = sinon.stub(vscode.window, 'showInputBox').resolves('testFunctionName'); + + // prompt user to select sql connection string setting from local.settings.json + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); + + // promptAndUpdateConnectionStringSetting + inputStub.onSecondCall().resolves('SqlConnectionString'); + // promptConnectionStringPasswordAndUpdateConnectionString - tested in AzureFunctionUtils.test.ts + quickPickStub.onSecondCall().resolves((constants.yesString) as any); + testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); + + should(connectionInfo.database).equal('my_db', 'Initial ConnectionInfo database should be my_db'); + + // table node used when creating azure function project + let tableTestNode = createTestTableNode(connectionInfo); + await azureFunctionService.createAzureFunction(tableTestNode); + + should(showErrorMessageSpy.notCalled).be.true('showErrorMessage should not have been called'); + should(connectionInfo.database).equal('testDb', 'connectionInfo.database should be testDb used from the test view node from OE'); + }); + }); }); describe('Cancel/Error scenarios for Azure Function with SQL Binding ', function (): void { @@ -218,11 +294,12 @@ describe('AzureFunctionsService', () => { sinon.stub(azureFunctionUtils, 'getAzureFunctionsExtensionApi').resolves(testUtils.azureFunctionsExtensionApi.object); // set azure functions extension api sinon.stub(azureFunctionUtils, 'getAzureFunctionProject').resolves(projectFilePath); //set azure function project to have one project sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); - - // select input or output binding - quickPickStub = sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.input, type: BindingType.input }); }); + it('Should prompt connection profile when user cancels selecting database', async function (): Promise { + // select view for object type + quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: constants.input, type: ObjectType.View } as any); + // This test will have an azure function project already in the project and the azure functions extension installed (stubbed) let connectionInfo: IConnectionInfo = createTestCredentials();// create test connectionInfo @@ -241,7 +318,10 @@ describe('AzureFunctionsService', () => { should(promptForConnectionStub.callCount).equal(2, 'promptForConnection should have been called 2 times only'); }); - it('Should prompt connection profile when user cancels selecting table', async function (): Promise { + it('Should prompt connection profile when user cancels selecting object from object lists', async function (): Promise { + // select view for object type + quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: constants.input, type: ObjectType.View } as any); + // This test will re-prompt the user to choose connection profile let connectionInfo: IConnectionInfo = createTestCredentials();// create test connectionInfo @@ -253,10 +333,10 @@ describe('AzureFunctionsService', () => { // select the testDB for promptForDatabase quickPickStub.onSecondCall().resolves(('testDb') as any); - // get tables from selected database - const params = { ownerUri: 'testConnectionURI', queryString: azureFunctionUtils.tablesQuery('testDb') }; + // get views from selected database + const params = { ownerUri: 'testConnectionURI', queryString: azureFunctionUtils.viewsQuery('testDb') }; testUtils.vscodeMssqlIExtension.setup(x => x.sendRequest(azureFunctionsContracts.SimpleExecuteRequest.type, params)) - .returns(() => Promise.resolve({ rowCount: 1, columnInfo: [], rows: [[{ displayValue: '[schema].[testTable]' }]] })); + .returns(() => Promise.resolve({ rowCount: 1, columnInfo: [], rows: [[{ displayValue: '[schema].[testView]' }]] })); // cancel out of promptForTables - select table to use quickPickStub.onThirdCall().resolves(undefined); @@ -270,31 +350,34 @@ describe('AzureFunctionsService', () => { }); it('Should prompt select table when user cancels out of manually entering table', async function (): Promise { + // select table for object type + quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: constants.input, type: ObjectType.Table } as any); + quickPickStub.onSecondCall().resolves({ label: constants.input, type: BindingType.input }); // This test will have an azure function project already in the project and the azure functions extension installed (stubbed) let connectionInfo: IConnectionInfo = createTestCredentials();// create test connectionInfo - // no table used for connection info so prompt user to get connection info + // no connection node used for connection info so prompt user to get connection info // promptForConnection is set first time for user let promptForConnectionStub = sinon.stub(testUtils.vscodeMssqlIExtension.object, 'promptForConnection').withArgs(true).onFirstCall().resolves(connectionInfo); // setup listDatabases request with connectionURI testUtils.vscodeMssqlIExtension.setup(x => x.connect(connectionInfo)).returns(() => Promise.resolve('testConnectionURI')); testUtils.vscodeMssqlIExtension.setup(x => x.listDatabases('testConnectionURI')).returns(() => Promise.resolve(['testDb'])); // select the testDB from list of databases based on connection info - quickPickStub.onSecondCall().resolves(('testDb') as any); + quickPickStub.onThirdCall().resolves(('testDb') as any); // get tables from selected database 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: [[{ displayValue: '[schema].[testTable]' }]] })); // select the option to manually enter table name - let manuallyEnterObjectName = constants.manuallyEnterObjectName(constants.enterObjectName); - quickPickStub.onThirdCall().resolves(manuallyEnterObjectName as any); + let manuallyEnterObjectName = constants.manuallyEnterObjectName(constants.enterTableName); + quickPickStub.onCall(3).resolves(manuallyEnterObjectName as any); // cancel out of manually enter inputBox sinon.stub(vscode.window, 'showInputBox').resolves(undefined); // resolve promises to undefined to exit out of createFunction quickPickStub.onCall(4).resolves(undefined); promptForConnectionStub.onSecondCall().resolves(undefined); - should(connectionInfo.database).equal('my_db', 'ConnectionInfo database should not be changed'); + should(connectionInfo.database).equal('my_db', 'Initial ConnectionInfo database should be my_db'); await azureFunctionService.createAzureFunction(); should(connectionInfo.database).equal('testDb', 'ConnectionInfo database should be user selected database'); @@ -308,8 +391,52 @@ describe('AzureFunctionsService', () => { ); }); + it('Should prompt select view when user cancels out of manually entering view', async function (): Promise { + // select view for object type + quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: constants.input, type: ObjectType.View } as any); + // This test will have an azure function project already in the project and the azure functions extension installed (stubbed) + let connectionInfo: IConnectionInfo = createTestCredentials();// create test connectionInfo + + // no connection node used for connection info so prompt user to get connection info + // promptForConnection is set first time for user + let promptForConnectionStub = sinon.stub(testUtils.vscodeMssqlIExtension.object, 'promptForConnection').withArgs(true).onFirstCall().resolves(connectionInfo); + // setup listDatabases request with connectionURI + testUtils.vscodeMssqlIExtension.setup(x => x.connect(connectionInfo)).returns(() => Promise.resolve('testConnectionURI')); + testUtils.vscodeMssqlIExtension.setup(x => x.listDatabases('testConnectionURI')).returns(() => Promise.resolve(['testDb'])); + // select the testDB from list of databases based on connection info + quickPickStub.onSecondCall().resolves(('testDb') as any); + // get tables from selected database + const params = { ownerUri: 'testConnectionURI', queryString: azureFunctionUtils.viewsQuery('testDb') }; + testUtils.vscodeMssqlIExtension.setup(x => x.sendRequest(azureFunctionsContracts.SimpleExecuteRequest.type, params)) + .returns(() => Promise.resolve({ rowCount: 1, columnInfo: [], rows: [[{ displayValue: '[schema].[testView]' }]] })); + // select the option to manually enter table name + let manuallyEnterObjectName = constants.manuallyEnterObjectName(constants.enterViewName); + quickPickStub.onThirdCall().resolves(manuallyEnterObjectName as any); + // cancel out of manually enter inputBox + sinon.stub(vscode.window, 'showInputBox').resolves(undefined); + // resolve promises to undefined to exit out of createFunction + quickPickStub.onCall(3).resolves(undefined); + promptForConnectionStub.onSecondCall().resolves(undefined); + + should(connectionInfo.database).equal('my_db', 'Initial ConnectionInfo database should be my_db'); + await azureFunctionService.createAzureFunction(); + + should(connectionInfo.database).equal('testDb', 'ConnectionInfo database should be user selected database'); + should(quickPickStub.getCall(3).args).containDeepOrdered([ + [manuallyEnterObjectName, '[schema].[testView]'], + { + canPickMany: false, + title: constants.selectView, + ignoreFocusOut: true + }] + ); + }); + it('Should prompt for connection profile if connection throws connection error', async function (): Promise { - // no table used for connection info so prompt user to get connection info + // select view for object type + quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: constants.input, type: ObjectType.View } as any); + + // no connection node used for connection info so prompt user to get connection info // promptForConnection is selected first time for user and then set undefined in order to exit out of the createFunction let promptForConnectionStub = sinon.stub(testUtils.vscodeMssqlIExtension.object, 'promptForConnection').withArgs(true).throws('Error connecting to connection profile'); promptForConnectionStub.onSecondCall().resolves(undefined);