diff --git a/extensions/sql-bindings/src/common/azureFunctionsUtils.ts b/extensions/sql-bindings/src/common/azureFunctionsUtils.ts index 865123c1b2..5edf8ca23c 100644 --- a/extensions/sql-bindings/src/common/azureFunctionsUtils.ts +++ b/extensions/sql-bindings/src/common/azureFunctionsUtils.ts @@ -362,87 +362,43 @@ export async function promptAndUpdateConnectionStringSetting(projectUri: vscode. // show the settings from project's local.settings.json if there's an AF functions project if (projectUri) { - let settings; - try { - settings = await getLocalSettingsJson(path.join(path.dirname(projectUri.fsPath!), constants.azureFunctionLocalSettingsFileName)); - } catch (e) { - void vscode.window.showErrorMessage(utils.getErrorMessage(e)); - return; - } + // get existing connection string settings from project's local.settings.json file + // if an error occurs getLocalSettingsJson will throw an error + let existingSettings = await getLocalSettingsJson(path.join(path.dirname(projectUri.fsPath!), constants.azureFunctionLocalSettingsFileName)); - // Known Azure settings reference for Azure Functions - // https://docs.microsoft.com/en-us/azure/azure-functions/functions-app-settings - const knownSettings: string[] = [ - 'APPINSIGHTS_INSTRUMENTATIONKEY', - 'APPLICATIONINSIGHTS_CONNECTION_STRING', - 'AZURE_FUNCTION_PROXY_DISABLE_LOCAL_CALL', - 'AZURE_FUNCTION_PROXY_BACKEND_URL_DECODE_SLASHES', - 'AZURE_FUNCTIONS_ENVIRONMENT', - 'AzureWebJobsDashboard', - 'AzureWebJobsDisableHomepage', - 'AzureWebJobsDotNetReleaseCompilation', - 'AzureWebJobsFeatureFlags', - 'AzureWebJobsKubernetesSecretName', - 'AzureWebJobsSecretStorageKeyVaultClientId', - 'AzureWebJobsSecretStorageKeyVaultClientSecret', - 'AzureWebJobsSecretStorageKeyVaultName', - 'AzureWebJobsSecretStorageKeyVaultTenantId', - 'AzureWebJobsSecretStorageKeyVaultUri', - 'AzureWebJobsSecretStorageSas', - 'AzureWebJobsSecretStorageType', - 'AzureWebJobsStorage', - 'AzureWebJobs_TypeScriptPath', - 'DOCKER_SHM_SIZE', - 'FUNCTION_APP_EDIT_MODE', - 'FUNCTIONS_EXTENSION_VERSION', - 'FUNCTIONS_V2_COMPATIBILITY_MODE', - 'FUNCTIONS_WORKER_PROCESS_COUNT', - 'FUNCTIONS_WORKER_RUNTIME', - 'FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED', - 'MDMaxBackgroundUpgradePeriod', - 'MDNewSnapshotCheckPeriod', - 'MDMinBackgroundUpgradePeriod', - 'PIP_EXTRA_INDEX_URL', - 'PYTHON_ISOLATE_WORKER_DEPENDENCIES (Preview)', - 'PYTHON_ENABLE_DEBUG_LOGGING', - 'PYTHON_ENABLE_WORKER_EXTENSIONS', - 'PYTHON_THREADPOOL_THREAD_COUNT', - 'SCALE_CONTROLLER_LOGGING_ENABLED', - 'SCM_LOGSTREAM_TIMEOUT', - 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING', - 'WEBSITE_CONTENTOVERVNET', - 'WEBSITE_CONTENTSHARE', - 'WEBSITE_SKIP_CONTENTSHARE_VALIDATION', - 'WEBSITE_DNS_SERVER', - 'WEBSITE_ENABLE_BROTLI_ENCODING', - 'WEBSITE_MAX_DYNAMIC_APPLICATION_SCALE_OUT', - 'WEBSITE_NODE_DEFAULT_VERSION', - 'WEBSITE_RUN_FROM_PACKAGE', - 'WEBSITE_TIME_ZONE', - 'WEBSITE_VNET_ROUTE_ALL' - ]; - - // setup connetion string setting quickpick + // setup connection string setting quickpick let connectionStringSettings: (vscode.QuickPickItem)[] = []; - if (settings?.Values) { - connectionStringSettings = Object.keys(settings.Values).filter(setting => !knownSettings.includes(setting)).map(setting => { return { label: setting }; }); + let hasNonFilteredSettings: boolean = false; + if (existingSettings?.Values && Object.keys(existingSettings?.Values!).length > 0) { + // add settings found in local.settings.json to quickpick list + connectionStringSettings = Object.keys(existingSettings.Values).filter(setting => !constants.knownSettings.includes(setting)).map(setting => { return { label: setting }; }); + // set boolean to true if there are non-filtered settings + hasNonFilteredSettings = connectionStringSettings.length > 0; } + // add create new setting option to quickpick list connectionStringSettings.unshift({ label: constants.createNewLocalAppSettingWithIcon }); - let sqlConnectionStringSettingExists = connectionStringSettings.find(s => s.label === constants.sqlConnectionStringSetting); while (!connectionStringSettingName) { - const selectedSetting = await vscode.window.showQuickPick(connectionStringSettings, { - canPickMany: false, - title: constants.selectSetting, - ignoreFocusOut: true - }); - if (!selectedSetting) { - // User cancelled - return; + let selectedSetting: vscode.QuickPickItem | undefined; + // prompt user to select a setting from the list or create a new one + // only if there are existing setting values are found and has non-filtered settings + if (hasNonFilteredSettings) { + selectedSetting = await vscode.window.showQuickPick(connectionStringSettings, { + canPickMany: false, + title: constants.selectSetting, + ignoreFocusOut: true + }); + if (!selectedSetting) { + // User cancelled + return; + } } - if (selectedSetting.label === constants.createNewLocalAppSettingWithIcon) { + // prompt user to enter connection string setting name if user selects create new setting or there is no existing settings in local.settings.json + if (selectedSetting?.label === constants.createNewLocalAppSettingWithIcon || !hasNonFilteredSettings) { + let sqlConnectionStringSettingExists = connectionStringSettings.find(s => s.label === constants.sqlConnectionStringSetting); + // prompt user to enter connection string setting name manually const newConnectionStringSettingName = await vscode.window.showInputBox( { title: constants.enterConnectionStringSettingName, @@ -452,9 +408,13 @@ export async function promptAndUpdateConnectionStringSetting(projectUri: vscode. } ) ?? ''; - if (!newConnectionStringSettingName) { - // go back to select setting quickpick if user escapes from inputting the setting name in case they changed their mind + if (!newConnectionStringSettingName && hasNonFilteredSettings) { + // go back to select setting quickpick if user escapes from entering in the connection string setting name + // only go back if there are existing settings in local.settings.json continue; + } else if (!newConnectionStringSettingName && !hasNonFilteredSettings) { + // User cancelled out of the manually enter connection string prompt + return; } let selectedConnectionStringMethod: string | undefined; @@ -477,7 +437,7 @@ export async function promptAndUpdateConnectionStringSetting(projectUri: vscode. return; } if (selectedConnectionStringMethod === constants.userConnectionString) { - // User chooses to enter connection string manually + // prompt user to enter connection string manually connectionString = await vscode.window.showInputBox( { title: constants.enterConnectionString, @@ -515,19 +475,19 @@ export async function promptAndUpdateConnectionStringSetting(projectUri: vscode. connectionStringSettingName = newConnectionStringSettingName; break; } else { - void vscode.window.showErrorMessage(constants.selectConnectionError()); + void vscode.window.showErrorMessage(constants.failedToSetSetting()); } } catch (e) { // display error message and show select setting quickpick again - void vscode.window.showErrorMessage(constants.selectConnectionError(e)); + void vscode.window.showErrorMessage(constants.failedToSetSetting(e)); continue; } } } else { // If user cancels out of this or doesn't want to overwrite an existing setting // just return them to the select setting quickpick in case they changed their mind - connectionStringSettingName = selectedSetting.label; + connectionStringSettingName = selectedSetting?.label; } } // Add sql extension package reference to project. If the reference is already there, it doesn't get added again diff --git a/extensions/sql-bindings/src/common/constants.ts b/extensions/sql-bindings/src/common/constants.ts index 64f5292383..a2e91754bb 100644 --- a/extensions/sql-bindings/src/common/constants.ts +++ b/extensions/sql-bindings/src/common/constants.ts @@ -78,10 +78,62 @@ export const enterObjectName = localize('enterObjectName', 'Enter SQL table or v export const enterObjectNameToUpsert = localize('enterObjectNameToUpsert', 'Enter SQL table to upsert into'); export const selectTable = localize('selectTable', 'Select table to use'); export const tableListProgressTitle = localize('tableListProgressTitle', "Fetching tables for selected database..."); -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 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); } export function failedToParse(filename: string, error: any): string { return localize('failedToParse', 'Failed to parse "{0}": {1}.', filename, utils.getErrorMessage(error)); } export function addSqlBinding(functionName: string): string { return localize('addSqlBinding', 'Adding SQL Binding to function "{0}"...'), functionName; } export function errorNewAzureFunction(error: any): string { return localize('errorNewAzureFunction', 'Error creating new Azure Function: {0}', utils.getErrorMessage(error)); } export function manuallyEnterObjectName(userObjectName: string): string { return `$(pencil) ${userObjectName}`; } + +// Known Azure settings reference for Azure Functions +// https://docs.microsoft.com/en-us/azure/azure-functions/functions-app-settings +export const knownSettings: string[] = [ + 'APPINSIGHTS_INSTRUMENTATIONKEY', + 'APPLICATIONINSIGHTS_CONNECTION_STRING', + 'AZURE_FUNCTION_PROXY_DISABLE_LOCAL_CALL', + 'AZURE_FUNCTION_PROXY_BACKEND_URL_DECODE_SLASHES', + 'AZURE_FUNCTIONS_ENVIRONMENT', + 'AzureWebJobsDashboard', + 'AzureWebJobsDisableHomepage', + 'AzureWebJobsDotNetReleaseCompilation', + 'AzureWebJobsFeatureFlags', + 'AzureWebJobsKubernetesSecretName', + 'AzureWebJobsSecretStorageKeyVaultClientId', + 'AzureWebJobsSecretStorageKeyVaultClientSecret', + 'AzureWebJobsSecretStorageKeyVaultName', + 'AzureWebJobsSecretStorageKeyVaultTenantId', + 'AzureWebJobsSecretStorageKeyVaultUri', + 'AzureWebJobsSecretStorageSas', + 'AzureWebJobsSecretStorageType', + 'AzureWebJobsStorage', + 'AzureWebJobs_TypeScriptPath', + 'DOCKER_SHM_SIZE', + 'FUNCTION_APP_EDIT_MODE', + 'FUNCTIONS_EXTENSION_VERSION', + 'FUNCTIONS_V2_COMPATIBILITY_MODE', + 'FUNCTIONS_WORKER_PROCESS_COUNT', + 'FUNCTIONS_WORKER_RUNTIME', + 'FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED', + 'MDMaxBackgroundUpgradePeriod', + 'MDNewSnapshotCheckPeriod', + 'MDMinBackgroundUpgradePeriod', + 'PIP_EXTRA_INDEX_URL', + 'PYTHON_ISOLATE_WORKER_DEPENDENCIES (Preview)', + 'PYTHON_ENABLE_DEBUG_LOGGING', + 'PYTHON_ENABLE_WORKER_EXTENSIONS', + 'PYTHON_THREADPOOL_THREAD_COUNT', + 'SCALE_CONTROLLER_LOGGING_ENABLED', + 'SCM_LOGSTREAM_TIMEOUT', + 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING', + 'WEBSITE_CONTENTOVERVNET', + 'WEBSITE_CONTENTSHARE', + 'WEBSITE_SKIP_CONTENTSHARE_VALIDATION', + 'WEBSITE_DNS_SERVER', + 'WEBSITE_ENABLE_BROTLI_ENCODING', + 'WEBSITE_MAX_DYNAMIC_APPLICATION_SCALE_OUT', + 'WEBSITE_NODE_DEFAULT_VERSION', + 'WEBSITE_RUN_FROM_PACKAGE', + 'WEBSITE_TIME_ZONE', + 'WEBSITE_VNET_ROUTE_ALL' +]; diff --git a/extensions/sql-bindings/src/test/common/azureFunctionsUtils.test.ts b/extensions/sql-bindings/src/test/common/azureFunctionsUtils.test.ts index 105d29588e..70b550c858 100644 --- a/extensions/sql-bindings/src/test/common/azureFunctionsUtils.test.ts +++ b/extensions/sql-bindings/src/test/common/azureFunctionsUtils.test.ts @@ -26,36 +26,27 @@ describe('AzureFunctionUtils', function (): void { }); describe('Local.Settings.Json', function (): void { - it('Should correctly parse local.settings.json', async () => { + beforeEach(function (): void { + // create fake connection string settings for local.setting.json to be used sinon.stub(fs.promises, 'access').onFirstCall().resolves(); sinon.stub(fs, 'readFileSync').withArgs(localSettingsPath).returns( `{"IsEncrypted": false, - "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` + "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` ); + }); + it('Should correctly parse local.settings.json', async () => { let settings = await azureFunctionsUtils.getLocalSettingsJson(localSettingsPath); should(settings.IsEncrypted).equals(false); should(Object.keys(settings.Values!).length).equals(3); }); it('setLocalAppSetting can update settings.json with new setting value', async () => { - sinon.stub(fs.promises, 'access').onFirstCall().resolves(); - sinon.stub(fs, 'readFileSync').withArgs(localSettingsPath).returns( - `{"IsEncrypted": false, - "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` - ); - let writeFileStub = sinon.stub(fs.promises, 'writeFile').resolves(); await azureFunctionsUtils.setLocalAppSetting(path.dirname(localSettingsPath), 'test4', 'test4'); should(writeFileStub.calledWithExactly(localSettingsPath, `{\n "IsEncrypted": false,\n "Values": {\n "test1": "test1",\n "test2": "test2",\n "test3": "test3",\n "test4": "test4"\n }\n}`)).equals(true, 'writeFile should be called with the correct arguments'); }); it('Should not overwrite setting if value already exists in local.settings.json', async () => { - sinon.stub(fs.promises, 'access').onFirstCall().resolves(); - sinon.stub(fs, 'readFileSync').withArgs(localSettingsPath).returns( - `{"IsEncrypted": false, - "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` - ); - let warningMsg = constants.settingAlreadyExists('test1'); const showErrorMessageSpy = sinon.stub(vscode.window, 'showWarningMessage').resolves({ title: constants.settingAlreadyExists('test1') }); @@ -70,11 +61,6 @@ describe('AzureFunctionUtils', function (): void { }); it('Should add connection string to local.settings.json', async () => { - sinon.stub(fs.promises, 'access').onFirstCall().resolves(); - sinon.stub(fs, 'readFileSync').withArgs(localSettingsPath).returns( - `{"IsEncrypted": false, - "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` - ); const connectionString = 'testConnectionString'; let writeFileStub = sinon.stub(fs.promises, 'writeFile').resolves(); @@ -84,6 +70,14 @@ describe('AzureFunctionUtils', function (): void { }); describe('Password Prompts', function (): void { + beforeEach(function (): void { + sinon.stub(fs.promises, 'access').onFirstCall().resolves(); + sinon.stub(fs, 'readFileSync').withArgs(localSettingsPath).returns( + `{"IsEncrypted": false, + "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` + ); + }); + it('Should include password if user includes password and connection info contains the password and auth type is SQL', async () => { sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); let connectionInfo: IConnectionInfo = createTestCredentials();// Mocks promptForConnection @@ -106,7 +100,7 @@ describe('AzureFunctionUtils', function (): void { should(quickInputSpy.notCalled).be.true('showInputBox should not have been called'); should(warningSpy.notCalled).be.true('showWarningMessage should not have been called'); // get connection string result - should(getConnectionString).equals(`Server=${connectionInfo.server};Initial Catalog=${connectionInfo.database};User ID=${connectionInfo.user};Password=${connectionInfo.password};`,'Should return a connection string with the password'); + should(getConnectionString).equals(`Server=${connectionInfo.server};Initial Catalog=${connectionInfo.database};User ID=${connectionInfo.user};Password=${connectionInfo.password};`, 'Should return a connection string with the password'); }); it('Should not include password and show warning if user does not want to include password prompt and connection info contains the password and auth type is SQL', async () => { @@ -131,7 +125,7 @@ describe('AzureFunctionUtils', function (): void { should(quickInputSpy.notCalled).be.true('showInputBox should not have been called'); should(warningSpy.calledOnce).be.true('showWarningMessage should have been called'); // returned connection string should NOT include password - should(getConnectionString).equals(`Server=${connectionInfo.server};Initial Catalog=${connectionInfo.database};User ID=${connectionInfo.user};Password=${constants.passwordPlaceholder};`,'Should return a connection string without the password'); + should(getConnectionString).equals(`Server=${connectionInfo.server};Initial Catalog=${connectionInfo.database};User ID=${connectionInfo.user};Password=${constants.passwordPlaceholder};`, 'Should return a connection string without the password'); }); it('Should not include password and show warning if user cancels include password prompt and connection info contains the password and auth type is SQL', async () => { @@ -301,40 +295,40 @@ describe('AzureFunctionUtils', function (): void { describe('PromptForObjectName', function (): void { it('Should prompt user to enter object name manually when no connection info given', async () => { - let promptStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('test'); + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('test'); let result = await azureFunctionsUtils.promptForObjectName(BindingType.input); - should(promptStub.calledOnce).be.true('showInputBox should have been called'); + should(inputBoxStub.calledOnce).be.true('showInputBox should have been called'); should(result).be.equal('test', 'Should return test since user manually entered object name'); }); it('Should return undefined when mssql connection error', async () => { sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); let connectionInfo: IConnectionInfo = createTestCredentials();// Mocks promptForConnection - let promptStub = sinon.stub(vscode.window, 'showInputBox'); + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox'); sinon.stub(azureFunctionsUtils, 'getConnectionURI').resolves(undefined); let result = await azureFunctionsUtils.promptForObjectName(BindingType.input, connectionInfo); - should(promptStub.notCalled).be.true('showInputBox should not have been called'); + should(inputBoxStub.notCalled).be.true('showInputBox should not have been called'); should(result).be.equal(undefined, 'Should return undefined due to mssql connection error'); }); it('Should return undefined if no database selected', async () => { sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); let connectionInfo: IConnectionInfo = createTestCredentials();// Mocks promptForConnection - let promptStub = sinon.stub(vscode.window, 'showInputBox'); + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox'); testUtils.vscodeMssqlIExtension.setup(x => x.connect(connectionInfo)).returns(() => Promise.resolve('testConnectionURI')); sinon.stub(vscode.window, 'showQuickPick').resolves(undefined); let result = await azureFunctionsUtils.promptForObjectName(BindingType.input, connectionInfo); - should(promptStub.notCalled).be.true('showInputBox should not have been called'); + should(inputBoxStub.notCalled).be.true('showInputBox should not have been called'); should(result).be.equal(undefined, 'Should return undefined due to no database selected'); }); it('Should successfully select object name', async () => { sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); let connectionInfo: IConnectionInfo = createTestCredentials();// Mocks promptForConnection - let promptStub = sinon.stub(vscode.window, 'showInputBox'); + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox'); // getConnectionURI stub testUtils.vscodeMssqlIExtension.setup(x => x.connect(connectionInfo)).returns(() => Promise.resolve('testConnectionURI')); // promptSelectDatabase stub @@ -349,13 +343,328 @@ describe('AzureFunctionUtils', function (): void { let result = await azureFunctionsUtils.promptForObjectName(BindingType.input, connectionInfo); - should(promptStub.notCalled).be.true('showInputBox should not have been called'); + should(inputBoxStub.notCalled).be.true('showInputBox should not have been called'); should(quickPickStub.calledTwice).be.true('showQuickPick should have been called twice'); should(connectionInfo.database).be.equal('testDb', 'Should have connectionInfo.database to testDb after user selects database'); should(result).be.equal('[schema].[testTable]', 'Should return [schema].[testTable] since user selected table'); }); }); + describe('PromptAndUpdateConnectionStringSetting', function (): void { + const fileUri = vscode.Uri.file(path.join(rootFolderPath, 'testProjectU')); + it('Should prompt user to enter connection string setting only if no azure function project uri given', async () => { + let quickPickStub = sinon.spy(vscode.window, 'showQuickPick'); + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('test'); + + let result = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(undefined); + should(quickPickStub.notCalled).be.true('quickPickStub should not have been called'); + should(inputBoxStub.calledOnce).be.true('showInputBox should have been called'); + should(result?.connectionStringSettingName).be.equal('test', 'Should return test since user manually entered connection string'); + should(inputBoxStub.firstCall.args).containEql( + { + prompt: constants.connectionStringSetting, + placeHolder: constants.connectionStringSettingPlaceholder, + ignoreFocusOut: true + } + ); + }); + + describe('No local.settings.json file', function (): void { + let fileAccessStub: sinon.SinonStub; + + beforeEach(function (): void { + // stubs for getLocalSettingsJson calls + // returns {IsEncrypted: False} + fileAccessStub = sinon.stub(fs.promises, 'access').onFirstCall().rejects(); + }); + + it('Should prompt user to enter connection string setting name no connection info given', async () => { + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('testConnectionStringName'); // enter connection string setting name + + // no connection info given so prompt for connection info stubs + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves((constants.connectionProfile) as any); + sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); + testUtils.vscodeMssqlIExtension.setup(x => x.promptForConnection(true)).returns(() => Promise.resolve(connectionInfo)); + // passsword prompt stub + quickPickStub.onSecondCall().resolves((constants.yesString) as any); + // getConnectionString stubs - in password prompt logic + let connectionInfo: IConnectionInfo = createTestCredentials(); // create test connectionInfo + let connectionDetails = { options: connectionInfo }; + testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); + + // setLocalAppSetting stubs + fileAccessStub.onSecondCall().rejects(); // getLocalSettingsJson stub + // fails if we dont set writeFile stub + sinon.stub(fs.promises, 'writeFile').resolves(); + sinon.stub(azureFunctionsUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'testConnectionStringName', 'testConnectionString').resolves((true)); + // addSqlNugetReferenceToProjectFile stub + sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package'); + + let result = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(fileUri); + + should(inputBoxStub.calledOnce).be.true('showInputBox should have been called'); + should(quickPickStub.callCount).be.equal(2, 'quickPickStub should have been called'); + should(result?.connectionStringSettingName).be.equal('testConnectionStringName', 'Should return testConnectionStringName from manually entered connection string name'); + }); + + it('Should prompt user to enter connection string setting name when connection info given', async () => { + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('testConnectionStringName'); // enter connection string setting name + + // password prompt stub + sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves((constants.yesString) as any); + // getConnectionString stubs - in password prompt logic + let connectionInfo: IConnectionInfo = createTestCredentials(); // create test connectionInfo + let connectionDetails = { options: connectionInfo }; + testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); + + // setLocalAppSetting stubs + fileAccessStub.onSecondCall().rejects(); // getLocalSettingsJson stub + // fails if we dont set writeFile stub + sinon.stub(fs.promises, 'writeFile').resolves(); + sinon.stub(azureFunctionsUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'testConnectionStringName', 'testConnectionString').resolves((true)); + // addSqlNugetReferenceToProjectFile stub + sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package'); + + let result = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(fileUri, connectionInfo); + + should(inputBoxStub.calledOnce).be.true('showInputBox should have been called'); + should(quickPickStub.callCount).be.equal(1, 'quickPickStub should have been called'); + should(result?.connectionStringSettingName).be.equal('testConnectionStringName', 'Should return testConnectionStringName from manually entered connection string name'); + should(result?.connectionInfo).be.equal(connectionInfo, 'Should return connectionInfo'); + }); + + it('Should return when user cancels out of manually entering connection string name prompt and has no existing connection string in local.settings.json', async () => { + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves(undefined); // user cancels out of connection string setting name + let quickPickSpy = sinon.spy(vscode.window, 'showQuickPick'); + + let result = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(fileUri); + + should(inputBoxStub.calledOnce).be.true('showInputBox should have been called'); + should(quickPickSpy.callCount).be.equal(0, 'quickPickStub should have been called'); + should(result?.connectionStringSettingName).be.equal(undefined, 'Should return undefined since user cancelled out of connection string setting name prompt'); + }); + + }); + + describe('local.settings.json file contains non-filtered connection setting tests', function (): void { + + beforeEach(function (): void { + // create fake connection string settings for local.setting.json to be used + // getLocalSettingsJson stub + sinon.stub(fs.promises, 'access').resolves(); + sinon.stub(fs, 'readFileSync').withArgs(sinon.match.any).returns( + `{"IsEncrypted": false, + "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` + ); + }); + + it('Should use user entered connection string setting name when non-filtered connection strings local.settings.json', async () => { + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); // user chooses to create new connection string setting name + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('testConnectionStringName'); // enter connection string setting name + + // password prompt stub + sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); + quickPickStub.onSecondCall().resolves((constants.yesString) as any); + // getConnectionString stubs - in password prompt logic + let connectionInfo: IConnectionInfo = createTestCredentials(); // create test connectionInfo + let connectionDetails = { options: connectionInfo }; + testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); + + // fails if we dont set writeFile stub + sinon.stub(fs.promises, 'writeFile').resolves(); + sinon.stub(azureFunctionsUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'testConnectionStringName', 'testConnectionString').resolves((true)); + // addSqlNugetReferenceToProjectFile stub + sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package'); + + let result = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(fileUri, connectionInfo); + + should(inputBoxStub.calledOnce).be.true('showInputBox should have been called'); + should(quickPickStub.callCount).be.equal(2, 'showQuickPick should have been called'); + should(result?.connectionStringSettingName).be.equal('testConnectionStringName', 'Should return testConnectionStringName from manually entered connection string name'); + should(result?.connectionInfo).be.equal(connectionInfo, 'Should return connectionInfo'); + }); + + it('Should use existing connection string when there are non-filtered connection strings found in local.settings.json', async () => { + let inputBoxSpy = sinon.spy(vscode.window, 'showInputBox'); + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: 'test1' }); // user chooses existing setting name + + // addSqlNugetReferenceToProjectFile stub + sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package'); + let connectionInfo: IConnectionInfo = createTestCredentials(); // create test connectionInfo + + let result = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(fileUri, connectionInfo); + + should(inputBoxSpy.notCalled).be.true('showInputBox should not have been called'); + should(quickPickStub.callCount).be.equal(1, 'showQuickPick should have been called'); + should(result?.connectionStringSettingName).be.equal('test1', 'Should return test1 setting chosen from quickpick'); + should(result?.connectionInfo).be.equal(connectionInfo, 'Should return connectionInfo'); + }); + + it('Should use user entered connection string setting name and manually enter connection string when no connection info given', async () => { + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); // user chooses to create new connection string setting name + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('testConnectionStringName'); // enter connection string setting name + + // user chooses to manually enter connection string + quickPickStub.onSecondCall().resolves((constants.userConnectionString) as any); + inputBoxStub.onSecondCall().resolves('testConnectionString'); + + // setLocalAppSetting stubs + // fails if we dont set writeFile stub + sinon.stub(fs.promises, 'writeFile').resolves(); + sinon.stub(azureFunctionsUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'testConnectionStringName', 'testConnectionString').resolves((true)); + // addSqlNugetReferenceToProjectFile stub + sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package'); + + let result = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(fileUri); + + should(inputBoxStub.callCount).be.equal(2, 'showInputBox should have been called'); + should(quickPickStub.callCount).be.equal(2, 'showQuickPick should have been called'); + should(result?.connectionStringSettingName).be.equal('testConnectionStringName', 'Should return testConnectionStringName from manually entered connection string name'); + }); + + it('Should prompt connection string method when user cancels out of selecting connection profile', async () => { + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); // user chooses to create new connection string setting name + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('testConnectionStringName'); // enter connection string setting name + + // user chooses to manually enter connection string + quickPickStub.onSecondCall().resolves((constants.userConnectionString) as any); + inputBoxStub.onSecondCall().resolves(undefined); + + quickPickStub.onThirdCall().resolves(undefined); + + let result = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(fileUri); + + should(quickPickStub.getCall(1).args).containDeepOrdered([ + [constants.connectionProfile, constants.userConnectionString], + { + canPickMany: false, + title: constants.selectConnectionString, + ignoreFocusOut: true + }] + ); + should(inputBoxStub.callCount).be.equal(2, 'showInputBox should have been called twice'); + should(quickPickStub.callCount).be.equal(3, 'showQuickPick should have been called three times'); + should(result?.connectionStringSettingName).be.equal(undefined, 'Should return undefined since user cancelled out of connection string setting name prompt'); + }); + + it('Should prompt connection string method when user cancels out of manually entering connection string', async () => { + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); // user chooses to create new connection string setting name + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('testConnectionStringName'); // enter connection string setting name + + // user chooses to manually enter connection string + quickPickStub.onSecondCall().resolves((constants.connectionProfile) as any); + sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); + // user cancels out of connection profile prompt + testUtils.vscodeMssqlIExtension.setup(x => x.promptForConnection(true)).returns(() => Promise.resolve(undefined)); + + let result = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(fileUri); + + should(quickPickStub.getCall(2).args).containDeepOrdered([ + [constants.connectionProfile, constants.userConnectionString], + { + canPickMany: false, + title: constants.selectConnectionString, + ignoreFocusOut: true + }] + ); + should(inputBoxStub.callCount).be.equal(1, 'showInputBox should have been called'); + should(quickPickStub.callCount).be.equal(3, 'showQuickPick should have been called three times'); + should(result?.connectionStringSettingName).be.equal(undefined, 'Should return undefined since user cancelled out of connection string setting name prompt'); + }); + + it('Should prompt connection string settings when user cancels out of manually entering connection string name prompt', async () => { + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); // user chooses to create new connection string setting name + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves(undefined); // enter connection string setting name + + // cancel out of prompt for connection string settings + quickPickStub.onSecondCall().resolves(undefined); + + let result = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(fileUri); + + should(inputBoxStub.callCount).be.equal(1, 'showInputBox should have been called'); + should(quickPickStub.callCount).be.equal(2, 'showQuickPick should have been called'); + should(quickPickStub.getCall(1).args).containDeepOrdered([ + [{ label: constants.createNewLocalAppSettingWithIcon }, { label: 'test1' }, { label: 'test2' }, { label: 'test3' }], + { + canPickMany: false, + title: constants.selectSetting, + ignoreFocusOut: true + }] + ); + should(result?.connectionStringSettingName).be.equal(undefined, 'Should return undefined since user cancelled out of connection string setting name prompt'); + }); + }); + + it('Should prompt user to enter connection string setting name when local.settings.json values contains known connection strings', async () => { + // create fake connection string settings for local.setting.json to be used + // getLocalSettingsJson stub + sinon.stub(fs.promises, 'access').resolves(); + // known connection string values that will be filtered out + sinon.stub(fs, 'readFileSync').withArgs(sinon.match.any).returns( + `{"IsEncrypted": false, + "Values": {"AzureWebJobsStorage": "testWebJobStorage","WEBSITE_TIME_ZONE":"testTimeZone"}}` + ); + + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('testConnectionStringName'); // enter connection string setting name + + // password prompt stub + sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves((constants.yesString) as any); + // getConnectionString stubs - in password prompt logic + let connectionInfo: IConnectionInfo = createTestCredentials(); // create test connectionInfo + let connectionDetails = { options: connectionInfo }; + testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); + + // fails if we dont set writeFile stub + sinon.stub(fs.promises, 'writeFile').resolves(); + sinon.stub(azureFunctionsUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'testConnectionStringName', 'testConnectionString').resolves((true)); + // addSqlNugetReferenceToProjectFile stub + sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package'); + + let result = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(fileUri, connectionInfo); + + should(inputBoxStub.calledOnce).be.true('showInputBox should have been called'); + should(quickPickStub.callCount).be.equal(1, 'quickPickStub should have been called'); + should(result?.connectionStringSettingName).be.equal('testConnectionStringName', 'Should return testConnectionStringName from manually entered connection string name'); + should(result?.connectionInfo).be.equal(connectionInfo, 'Should return connectionInfo'); + }); + + it('Should prompt user to enter connection string setting name when local.settings.json values are empty', async () => { + // create fake connection string settings for local.setting.json to be used + // getLocalSettingsJson stub + sinon.stub(fs.promises, 'access').resolves(); + // empty values in local.settings.json + sinon.stub(fs, 'readFileSync').withArgs(sinon.match.any).returns( + `{"IsEncrypted": false, + "Values": {}}` + ); + + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('testConnectionStringName'); // enter connection string setting name + + // password prompt stub + sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves((constants.yesString) as any); + // getConnectionString stubs - in password prompt logic + let connectionInfo: IConnectionInfo = createTestCredentials(); // create test connectionInfo + let connectionDetails = { options: connectionInfo }; + testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); + + // fails if we dont set writeFile stub + sinon.stub(fs.promises, 'writeFile').resolves(); + sinon.stub(azureFunctionsUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'testConnectionStringName', 'testConnectionString').resolves((true)); + // addSqlNugetReferenceToProjectFile stub + sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package'); + + let result = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(fileUri, connectionInfo); + + should(inputBoxStub.calledOnce).be.true('showInputBox should have been called'); + should(quickPickStub.callCount).be.equal(1, 'quickPickStub should have been called'); + should(result?.connectionStringSettingName).be.equal('testConnectionStringName', 'Should return testConnectionStringName from manually entered connection string name'); + should(result?.connectionInfo).be.equal(connectionInfo, 'Should return connectionInfo'); + }); + }); + afterEach(function (): void { sinon.restore(); }); diff --git a/extensions/sql-bindings/src/test/dialog/addSqlBindingQuickpick.test.ts b/extensions/sql-bindings/src/test/dialog/addSqlBindingQuickpick.test.ts index 798ce2f527..41efcda3e1 100644 --- a/extensions/sql-bindings/src/test/dialog/addSqlBindingQuickpick.test.ts +++ b/extensions/sql-bindings/src/test/dialog/addSqlBindingQuickpick.test.ts @@ -24,6 +24,12 @@ const fileUri = vscode.Uri.file('testUri'); describe('Add SQL Binding quick pick', () => { beforeEach(function (): void { testUtils = createTestUtils(); + // 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"}}` + ); }); afterEach(function (): void { @@ -64,7 +70,7 @@ describe('Add SQL Binding quick pick', () => { // select input or output binding quickpickStub.onSecondCall().resolves({ label: constants.input, type: BindingType.input }); sinon.stub(azureFunctionUtils, 'getAFProjectContainingFile').resolves(vscode.Uri.file('testUri')); - // select connection profile - create new + // select connection string setting method - create new quickpickStub.onThirdCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); // give connection string setting name sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('sqlConnectionString'); @@ -189,7 +195,7 @@ describe('Add SQL Binding quick pick', () => { await launchAddSqlBindingQuickpick(vscode.Uri.file('testUri')); // should go back to the select connection string methods - should(quickpickStub.callCount).be.equal(5,'showQuickPick should have been called 5 times'); + should(quickpickStub.callCount).be.equal(5, 'showQuickPick should have been called 5 times'); should(quickpickStub.getCall(3).args).deepEqual([ [constants.connectionProfile, constants.userConnectionString], { @@ -225,7 +231,7 @@ describe('Add SQL Binding quick pick', () => { await launchAddSqlBindingQuickpick(vscode.Uri.file('testUri')); // should go back to the select connection string methods - should(quickpickStub.callCount).be.equal(4,'showQuickPick should have been called 4 times'); + should(quickpickStub.callCount).be.equal(4, 'showQuickPick should have been called 4 times'); should(quickpickStub.getCall(2).args).containDeepOrdered([ [{ label: constants.createNewLocalAppSettingWithIcon }], { @@ -267,7 +273,7 @@ describe('Add SQL Binding quick pick', () => { await launchAddSqlBindingQuickpick(vscode.Uri.file('testUri')); // should go back to the select connection string methods - should(quickpickStub.callCount).be.equal(5,'showQuickPick should have been called 5 times'); + should(quickpickStub.callCount).be.equal(5, 'showQuickPick should have been called 5 times'); should(quickpickStub.getCall(4).args).containDeepOrdered([ [constants.connectionProfile, constants.enterConnectionString], { diff --git a/extensions/sql-bindings/src/test/service/azureFunctionsService.test.ts b/extensions/sql-bindings/src/test/service/azureFunctionsService.test.ts index e26ffdf9d7..cfeed2d0cf 100644 --- a/extensions/sql-bindings/src/test/service/azureFunctionsService.test.ts +++ b/extensions/sql-bindings/src/test/service/azureFunctionsService.test.ts @@ -26,6 +26,7 @@ describe('AzureFunctionsService', () => { beforeEach(function (): void { testUtils = createTestUtils(); }); + describe('Create Azure Function with SQL Binding', () => { it('Should show info message to install azure functions extension if not installed', async function (): Promise { const infoStub = sinon.stub(vscode.window, 'showInformationMessage').resolves(undefined); @@ -41,7 +42,14 @@ describe('AzureFunctionsService', () => { sinon.stub(azureFunctionUtils, 'getAzureFunctionProject').resolves(projectFilePath); //set azure function project to have one project sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); - let connectionInfo: IConnectionInfo = createTestCredentials();// create test connectionInfo + // 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')); @@ -96,6 +104,13 @@ describe('AzureFunctionsService', () => { 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'));