diff --git a/extensions/sql-bindings/package.json b/extensions/sql-bindings/package.json index 9f15215ded..089b7f21f1 100644 --- a/extensions/sql-bindings/package.json +++ b/extensions/sql-bindings/package.json @@ -2,7 +2,7 @@ "name": "sql-bindings-vscode", "displayName": "%displayName%", "description": "%description%", - "version": "0.2.0", + "version": "0.3.0", "publisher": "ms-mssql", "preview": true, "engines": { diff --git a/extensions/sql-bindings/src/common/azureFunctionsUtils.ts b/extensions/sql-bindings/src/common/azureFunctionsUtils.ts index 81b404f052..bbbc803919 100644 --- a/extensions/sql-bindings/src/common/azureFunctionsUtils.ts +++ b/extensions/sql-bindings/src/common/azureFunctionsUtils.ts @@ -36,7 +36,7 @@ export interface IFileFunctionObject { * @returns settings in local.settings.json. If no settings are found, returns default "empty" settings */ export async function getLocalSettingsJson(localSettingsPath: string): Promise { - if (fs.existsSync(localSettingsPath)) { + if (await utils.exists(localSettingsPath)) { const data: string = (fs.readFileSync(localSettingsPath)).toString(); try { return JSON.parse(data); @@ -281,7 +281,7 @@ export async function getAFProjectContainingFile(fileUri: vscode.Uri): Promise { - return fs.existsSync(path.join(folderPath, constants.hostFileName)); + return await utils.exists(path.join(folderPath, constants.hostFileName)); } /** @@ -359,7 +359,6 @@ export async function promptForObjectName(bindingType: BindingType, connectionIn */ export async function promptAndUpdateConnectionStringSetting(projectUri: vscode.Uri | undefined, connectionInfo?: IConnectionInfo): Promise { let connectionStringSettingName: string | undefined; - const vscodeMssqlApi = await utils.getVscodeMssqlApi(); // show the settings from project's local.settings.json if there's an AF functions project if (projectUri) { @@ -494,6 +493,7 @@ export async function promptAndUpdateConnectionStringSetting(projectUri: vscode. } } else { // Let user choose from existing connections to create connection string from + const vscodeMssqlApi = await utils.getVscodeMssqlApi(); connectionInfo = await vscodeMssqlApi.promptForConnection(true); } } diff --git a/extensions/sql-bindings/src/common/utils.ts b/extensions/sql-bindings/src/common/utils.ts index 38f1257178..c11b9b6b06 100644 --- a/extensions/sql-bindings/src/common/utils.ts +++ b/extensions/sql-bindings/src/common/utils.ts @@ -116,7 +116,9 @@ export async function getUniqueFileName(fileName: string, folderPath?: string): let uniqueFileName = fileName; while (count < maxCount) { - if (!fs.existsSync(path.join(folderPath, uniqueFileName + '.cs'))) { + // checks to see if file exists + let uniqueFilePath = path.join(folderPath, uniqueFileName + '.cs'); + if (!(await exists(uniqueFilePath))) { return uniqueFileName; } count += 1; @@ -173,3 +175,12 @@ export function getErrorType(error: any): string | undefined { return 'UnknownError'; } } + +export async function exists(path: string): Promise { + try { + await fs.promises.access(path); + return true; + } catch (e) { + return false; + } +} diff --git a/extensions/sql-bindings/src/test/addConnectionStringStep.test.ts b/extensions/sql-bindings/src/test/addConnectionStringStep.test.ts index 1f0ecb8562..a9379833ee 100644 --- a/extensions/sql-bindings/src/test/addConnectionStringStep.test.ts +++ b/extensions/sql-bindings/src/test/addConnectionStringStep.test.ts @@ -2,19 +2,19 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; import * as should from 'should'; import * as sinon from 'sinon'; -import * as path from 'path'; -import * as fs from 'fs'; import * as TypeMoq from 'typemoq'; -import * as constants from '../common/constants'; +import * as vscode from 'vscode'; import * as azureFunctionUtils from '../common/azureFunctionsUtils'; +import * as constants from '../common/constants'; import * as utils from '../common/utils'; +import { IConnectionInfo } from 'vscode-mssql'; import { createAddConnectionStringStep } from '../createNewProject/addConnectionStringStep'; import { createTestCredentials, createTestUtils, TestUtils } from './testUtils'; -import { IConnectionInfo } from 'vscode-mssql'; const rootFolderPath = 'test'; const localSettingsPath: string = path.join(rootFolderPath, 'local.settings.json'); diff --git a/extensions/sql-bindings/src/test/common/azureFunctionsUtils.test.ts b/extensions/sql-bindings/src/test/common/azureFunctionsUtils.test.ts index b9646d2c46..0c843b5152 100644 --- a/extensions/sql-bindings/src/test/common/azureFunctionsUtils.test.ts +++ b/extensions/sql-bindings/src/test/common/azureFunctionsUtils.test.ts @@ -2,14 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; -import * as path from 'path'; import * as fs from 'fs'; +import * as path from 'path'; import * as should from 'should'; import * as sinon from 'sinon'; -import * as constants from '../../common/constants'; +import * as vscode from 'vscode'; import * as azureFunctionsUtils from '../../common/azureFunctionsUtils'; +import * as constants from '../../common/constants'; import * as utils from '../../common/utils'; +import * as azureFunctionsContracts from '../../contracts/azureFunctions/azureFunctionsContracts'; + +import { BindingType } from 'sql-bindings'; import { IConnectionInfo } from 'vscode-mssql'; import { createTestCredentials, createTestUtils, TestUtils } from '../testUtils'; @@ -24,7 +27,7 @@ describe('AzureFunctionUtils', function (): void { describe('Local.Settings.Json', function (): void { it('Should correctly parse local.settings.json', async () => { - sinon.stub(fs, 'existsSync').withArgs(localSettingsPath).returns(true); + sinon.stub(fs.promises, 'access').onFirstCall().resolves(); sinon.stub(fs, 'readFileSync').withArgs(localSettingsPath).returns( `{"IsEncrypted": false, "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` @@ -35,7 +38,7 @@ describe('AzureFunctionUtils', function (): void { }); it('setLocalAppSetting can update settings.json with new setting value', async () => { - sinon.stub(fs, 'existsSync').withArgs(localSettingsPath).returns(true); + sinon.stub(fs.promises, 'access').onFirstCall().resolves(); sinon.stub(fs, 'readFileSync').withArgs(localSettingsPath).returns( `{"IsEncrypted": false, "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` @@ -47,7 +50,7 @@ describe('AzureFunctionUtils', function (): void { }); it('Should not overwrite setting if value already exists in local.settings.json', async () => { - sinon.stub(fs, 'existsSync').withArgs(localSettingsPath).returns(true); + sinon.stub(fs.promises, 'access').onFirstCall().resolves(); sinon.stub(fs, 'readFileSync').withArgs(localSettingsPath).returns( `{"IsEncrypted": false, "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` @@ -67,7 +70,7 @@ describe('AzureFunctionUtils', function (): void { }); it('Should add connection string to local.settings.json', async () => { - sinon.stub(fs, 'existsSync').withArgs(localSettingsPath).returns(true); + sinon.stub(fs.promises, 'access').onFirstCall().resolves(); sinon.stub(fs, 'readFileSync').withArgs(localSettingsPath).returns( `{"IsEncrypted": false, "Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}` @@ -236,6 +239,123 @@ describe('AzureFunctionUtils', function (): void { }); }); + describe('Get Azure Function Project', function (): void { + it('Should return undefined if no azure function projects are found', async () => { + // set workspace folder for testing + sinon.replaceGetter(vscode.workspace, 'workspaceFolders', () => { + return [{ + uri: { + fsPath: '/temp/' + }, + }]; + }); + let findFilesStub = sinon.stub(vscode.workspace, 'findFiles'); + findFilesStub.onFirstCall().resolves([]); + findFilesStub.onSecondCall().resolves(undefined); + let result = await azureFunctionsUtils.getAzureFunctionProject(); + should(result).be.equal(undefined, 'Should be undefined since no azure function projects are found'); + }); + + it('Should return selectedProjectFile if only one azure function project is found', async () => { + // set workspace folder for testing + sinon.replaceGetter(vscode.workspace, 'workspaceFolders', () => { + return [{ + uri: { + fsPath: '/temp/' + }, + }]; + }); + // only one azure function project found - hostFiles and csproj files stubs + let findFilesStub = sinon.stub(vscode.workspace, 'findFiles'); + findFilesStub.onFirstCall().resolves([vscode.Uri.file('/temp/host.json')]); + findFilesStub.onSecondCall().returns(Promise.resolve([vscode.Uri.file('/temp/test.csproj')]) as any); + + let result = await azureFunctionsUtils.getAzureFunctionProject(); + should(result).be.equal('/temp/test.csproj', 'Should return test.csproj since only one Azure function project is found'); + }); + + it('Should return prompt to choose azure function project if multiple azure function projects are found', async () => { + // set workspace folder for testing + sinon.replaceGetter(vscode.workspace, 'workspaceFolders', () => { + return [{ + uri: { + fsPath: '/temp/' + }, + }]; + }); + // multiple azure function projects found in workspace - hostFiles and project find files stubs + let findFilesStub = sinon.stub(vscode.workspace, 'findFiles'); + findFilesStub.onFirstCall().returns(Promise.resolve([vscode.Uri.file('/temp/host.json'), vscode.Uri.file('/temp2/host.json')]) as any); + // we loop through the hostFiles to find the csproj in same directory + // first loop we use host of /temp/host.json + findFilesStub.onSecondCall().returns(Promise.resolve([vscode.Uri.file('/temp/test.csproj')]) as any); + // second loop we use host of /temp2/host.json + findFilesStub.onThirdCall().returns(Promise.resolve([vscode.Uri.file('/temp2/test.csproj')]) as any); + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').returns(Promise.resolve('/temp/test.csproj') as any); + + let result = await azureFunctionsUtils.getAzureFunctionProject(); + should(result).be.equal('/temp/test.csproj', 'Should return test.csproj since user choose Azure function project'); + should(quickPickStub.calledOnce).be.true('showQuickPick should have been called to choose between azure function projects'); + }); + }); + + 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 result = await azureFunctionsUtils.promptForObjectName(BindingType.input); + should(promptStub.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'); + 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(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'); + 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(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'); + // getConnectionURI stub + testUtils.vscodeMssqlIExtension.setup(x => x.connect(connectionInfo)).returns(() => Promise.resolve('testConnectionURI')); + // promptSelectDatabase stub + testUtils.vscodeMssqlIExtension.setup(x => x.listDatabases('testConnectionURI')).returns(() => Promise.resolve(['testDb'])); + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').resolves('testDb' as any); + // get tables from selected database + const params = { ownerUri: 'testConnectionURI', queryString: azureFunctionsUtils.tablesQuery('testDb') }; + testUtils.vscodeMssqlIExtension.setup(x => x.sendRequest(azureFunctionsContracts.SimpleExecuteRequest.type, params)) + .returns(() => Promise.resolve({ rowCount: 1, columnInfo: [], rows: [['[schema].[testTable]']] })); + // select the schema.testTable from list of tables based on connection info and database + quickPickStub.onSecondCall().returns(Promise.resolve('[schema].[testTable]') as any); + + let result = await azureFunctionsUtils.promptForObjectName(BindingType.input, connectionInfo); + + should(promptStub.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'); + }); + }); + afterEach(function (): void { sinon.restore(); }); diff --git a/extensions/sql-bindings/src/test/common/util.test.ts b/extensions/sql-bindings/src/test/common/util.test.ts new file mode 100644 index 0000000000..924facc96c --- /dev/null +++ b/extensions/sql-bindings/src/test/common/util.test.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; +import * as should from 'should'; +import * as sinon from 'sinon'; +import * as constants from '../../common/constants'; +import { getErrorType, getUniqueFileName, TimeoutError, validateFunctionName } from '../../common/utils'; + +describe('Utils', function (): void { + it('Should return undefined when no folderPath given to create unique file name', async () => { + let testFile = 'testFile'; + let result = await getUniqueFileName(testFile); + + should(result).be.equal(undefined, 'Should return undefined since no folderPath given'); + }); + + it('Should create unique file name if one exists', async () => { + let testFile = 'testFile'; + let testFolder = 'testFolder'; + let fileAccessStub = sinon.stub(fs.promises, 'access').onFirstCall().resolves(); + fileAccessStub.onSecondCall().throws(); + + let result = await getUniqueFileName(testFile, testFolder); + + should(result).be.equal('testFile1', 'Should return testFile1 since one testFile exists'); + }); + + it('Should create unique file name if multiple versions of the file exists', async () => { + let testFile = 'testFile'; + let testFolder = 'testFolder'; + let fileAccessStub = sinon.stub(fs.promises, 'access').onFirstCall().resolves(); + fileAccessStub.onSecondCall().resolves(); + fileAccessStub.onThirdCall().throws(); + + let result = await getUniqueFileName(testFile, testFolder); + + should(result).be.equal('testFile2', 'Should return testFile2 since both testFile1 and testFile exists'); + }); + + it('Should validate function name', async () => { + should(validateFunctionName('')).be.equal(constants.nameMustNotBeEmpty); + should(validateFunctionName('@$%@%@%')).be.equal(constants.hasSpecialCharacters); + should(validateFunctionName('test')).be.equal(undefined); + }); + + it('Should get error type', async () => { + should(getErrorType(new Error('test'))).be.equal('UnknownError'); + should(getErrorType(new TimeoutError('test'))).be.equal('TimeoutError'); + }); + + 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 18d35ce3d6..3ea8490acc 100644 --- a/extensions/sql-bindings/src/test/dialog/addSqlBindingQuickpick.test.ts +++ b/extensions/sql-bindings/src/test/dialog/addSqlBindingQuickpick.test.ts @@ -3,21 +3,21 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import * as should from 'should'; import * as sinon from 'sinon'; -import * as vscode from 'vscode'; import * as TypeMoq from 'typemoq'; -import * as fs from 'fs'; -import * as utils from '../../common/utils'; -import * as constants from '../../common/constants'; +import * as vscode from 'vscode'; import * as azureFunctionUtils from '../../common/azureFunctionsUtils'; -import * as azureFunctionService from '../../services/azureFunctionsService'; +import * as constants from '../../common/constants'; +import * as utils from '../../common/utils'; import * as azureFunctionsContracts from '../../contracts/azureFunctions/azureFunctionsContracts'; +import * as azureFunctionService from '../../services/azureFunctionsService'; -import { createTestUtils, TestUtils, createTestCredentials } from '../testUtils'; -import { launchAddSqlBindingQuickpick } from '../../dialogs/addSqlBindingQuickpick'; import { BindingType } from 'sql-bindings'; import { IConnectionInfo } from 'vscode-mssql'; +import { launchAddSqlBindingQuickpick } from '../../dialogs/addSqlBindingQuickpick'; +import { createTestCredentials, createTestUtils, TestUtils } from '../testUtils'; let testUtils: TestUtils; const fileUri = vscode.Uri.file('testUri'); @@ -30,30 +30,13 @@ describe('Add SQL Binding quick pick', () => { sinon.restore(); }); - it('Should show error if the file contains no Azure Functions', async function (): Promise { - sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); - sinon.stub(azureFunctionService, 'getAzureFunctions').withArgs(fileUri.fsPath).returns( - Promise.resolve({ - success: true, - errorMessage: '', - azureFunctions: [] - })); - const spy = sinon.spy(vscode.window, 'showErrorMessage'); - - await launchAddSqlBindingQuickpick(fileUri); - - const msg = constants.noAzureFunctionsInFile; - should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once'); - should(spy.calledWith(msg)).be.true(`showErrorMessage not called with expected message '${msg}' Actual '${spy.getCall(0).args[0]}'`); - }); - - it('Should show error if adding SQL binding was not successful', async function (): Promise { + it('Should successfully add SQL binding', async function (): Promise { sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); let connectionCreds: IConnectionInfo = createTestCredentials();// Mocks promptForConnection let connectionDetails = { options: connectionCreds }; // set test vscode-mssql API calls testUtils.vscodeMssqlIExtension.setup(x => x.promptForConnection(true)).returns(() => Promise.resolve(connectionCreds)); - testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString')); + testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString1')); testUtils.vscodeMssqlIExtension.setup(x => x.connect(connectionCreds)).returns(() => Promise.resolve('testConnectionURI')); testUtils.vscodeMssqlIExtension.setup(x => x.listDatabases('testConnectionURI')).returns(() => Promise.resolve(['testDb'])); const params = { ownerUri: 'testConnectionURI', queryString: azureFunctionUtils.tablesQuery('testDb') }; @@ -66,17 +49,15 @@ describe('Add SQL Binding quick pick', () => { errorMessage: '', azureFunctions: ['af1', 'af2'] })); - //failure since no AFs are found in the project - const errormsg = 'Error inserting binding'; sinon.stub(azureFunctionService, 'addSqlBinding').withArgs( sinon.match.any, sinon.match.any, sinon.match.any, sinon.match.any, sinon.match.any).returns( Promise.resolve({ - success: false, - errorMessage: errormsg + success: true, + errorMessage: '' })); - const spy = sinon.spy(vscode.window, 'showErrorMessage'); + const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); // select Azure function let quickpickStub = sinon.stub(vscode.window, 'showQuickPick').returns(Promise.resolve('af1') as any); @@ -91,18 +72,88 @@ describe('Add SQL Binding quick pick', () => { quickpickStub.onCall(4).returns(Promise.resolve(constants.yesString) as any); // setLocalAppSetting fails if we dont set writeFile stub sinon.stub(fs.promises, 'writeFile'); - sinon.stub(azureFunctionUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'sqlConnectionString', 'testConnectionString').returns(Promise.resolve(true)); + sinon.stub(azureFunctionUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'sqlConnectionString', 'testConnectionString1').returns(Promise.resolve(true)); sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package'); quickpickStub.onCall(5).returns(Promise.resolve('testDb') as any); quickpickStub.onCall(6).returns(Promise.resolve('[schema].[testTable]') as any); await launchAddSqlBindingQuickpick(vscode.Uri.file('testUri')); - should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once'); - should(spy.calledWith(errormsg)).be.true(`showErrorMessage not called with expected message '${errormsg}' Actual '${spy.getCall(0).args[0]}'`); + should(showErrorMessageSpy.notCalled).be.true('showErrorMessage should not be called'); }); - it('Should show error connection profile does not connect', async function (): Promise { + it('Should show error if adding SQL binding was not successful', async function (): Promise { + sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); + let connectionCreds: IConnectionInfo = createTestCredentials();// Mocks promptForConnection + let connectionDetails = { options: connectionCreds }; + // set test vscode-mssql API calls + testUtils.vscodeMssqlIExtension.setup(x => x.promptForConnection(true)).returns(() => Promise.resolve(connectionCreds)); + testUtils.vscodeMssqlIExtension.setup(x => x.getConnectionString(connectionDetails, true, false)).returns(() => Promise.resolve('testConnectionString2')); + testUtils.vscodeMssqlIExtension.setup(x => x.connect(connectionCreds)).returns(() => Promise.resolve('testConnectionURI')); + testUtils.vscodeMssqlIExtension.setup(x => x.listDatabases('testConnectionURI')).returns(() => Promise.resolve(['testDb'])); + const params = { ownerUri: 'testConnectionURI', queryString: azureFunctionUtils.tablesQuery('testDb') }; + testUtils.vscodeMssqlIExtension.setup(x => x.sendRequest(azureFunctionsContracts.SimpleExecuteRequest.type, params)) + .returns(() => Promise.resolve({ rowCount: 1, columnInfo: [], rows: [['[schema].[testTable]']] })); + + sinon.stub(azureFunctionService, 'getAzureFunctions').withArgs(fileUri.fsPath).returns( + Promise.resolve({ + success: true, + errorMessage: '', + azureFunctions: ['af1', 'af2'] + })); + //failure since no AFs are found in the project + const errormsg = 'Error inserting binding'; + sinon.stub(azureFunctionService, 'addSqlBinding').withArgs( + sinon.match.any, sinon.match.any, sinon.match.any, + sinon.match.any, sinon.match.any).returns( + Promise.resolve({ + success: false, + errorMessage: errormsg + })); + const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); + + // select Azure function + let quickpickStub = sinon.stub(vscode.window, 'showQuickPick').returns(Promise.resolve('af1') as any); + // select input or output binding + quickpickStub.onSecondCall().resolves({ label: constants.input, type: BindingType.input }); + sinon.stub(azureFunctionUtils, 'getAFProjectContainingFile').resolves(vscode.Uri.file('testUri')); + // select connection profile - create new + quickpickStub.onThirdCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); + // give connection string setting name + sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('sqlConnectionString'); + quickpickStub.onCall(3).returns(Promise.resolve(constants.connectionProfile) as any); + quickpickStub.onCall(4).returns(Promise.resolve(constants.yesString) as any); + // setLocalAppSetting fails if we dont set writeFile stub + sinon.stub(fs.promises, 'writeFile'); + sinon.stub(azureFunctionUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'sqlConnectionString', 'testConnectionString2').returns(Promise.resolve(true)); + sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package'); + quickpickStub.onCall(5).returns(Promise.resolve('testDb') as any); + quickpickStub.onCall(6).returns(Promise.resolve('[schema].[testTable]') as any); + + await launchAddSqlBindingQuickpick(vscode.Uri.file('testUri')); + + should(showErrorMessageSpy.calledOnce).be.true('showErrorMessage should have been called exactly once'); + should(showErrorMessageSpy.calledWith(errormsg)).be.true(`showErrorMessage not called with expected message '${errormsg}' Actual '${showErrorMessageSpy.getCall(0).args[0]}'`); + }); + + it('Should show error if the file contains no Azure Functions', async function (): Promise { + sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); + sinon.stub(azureFunctionService, 'getAzureFunctions').withArgs(fileUri.fsPath).returns( + Promise.resolve({ + success: true, + errorMessage: '', + azureFunctions: [] + })); + const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); + + await launchAddSqlBindingQuickpick(fileUri); + + const msg = constants.noAzureFunctionsInFile; + should(showErrorMessageSpy.calledOnce).be.true('showErrorMessage should have been called exactly once'); + should(showErrorMessageSpy.calledWith(msg)).be.true(`showErrorMessage not called with expected message '${msg}' Actual '${showErrorMessageSpy.getCall(0).args[0]}'`); + }); + + it('Should show error when connection profile does not connect', async function (): Promise { sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); let connectionCreds = createTestCredentials(); @@ -114,40 +165,116 @@ describe('Add SQL Binding quick pick', () => { azureFunctions: ['af1'] })); - // Mocks connect call to mssql - let error = new Error('Connection Request Failed'); - testUtils.vscodeMssqlIExtension.setup(x => x.connect(TypeMoq.It.isAny(), undefined)).throws(error); - // Mocks promptForConnection - testUtils.vscodeMssqlIExtension.setup(x => x.promptForConnection(true)).returns(() => Promise.resolve(connectionCreds)); let quickpickStub = sinon.stub(vscode.window, 'showQuickPick'); // select Azure function quickpickStub.onFirstCall().resolves({ label: 'af1' }); // select input or output binding quickpickStub.onSecondCall().resolves({ label: constants.input, type: BindingType.input }); - // give object name - let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('dbo.table1'); - // select connection string setting name quickpickStub.onThirdCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); // give connection string setting name - inputBoxStub.onSecondCall().resolves('SqlConnectionString'); + sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('SqlConnectionString'); // select connection profile method quickpickStub.onCall(3).resolves({ label: constants.connectionProfile }); + testUtils.vscodeMssqlIExtension.setup(x => x.promptForConnection(true)).returns(() => Promise.resolve(connectionCreds)); + + // Mocks connect call to mssql + let error = new Error('Connection Request Failed'); + testUtils.vscodeMssqlIExtension.setup(x => x.connect(TypeMoq.It.isAny(), undefined)).throws(error); await launchAddSqlBindingQuickpick(vscode.Uri.file('testUri')); // should go back to the select connection string methods - should(quickpickStub.callCount === 4); + should(quickpickStub.callCount).be.equal(5,'showQuickPick should have been called 5 times'); should(quickpickStub.getCall(3).args).deepEqual([ [constants.connectionProfile, constants.userConnectionString], { canPickMany: false, ignoreFocusOut: true, title: constants.selectConnectionString - }]); + }] + ); + }); + + it('Should show user connection string setting method after cancelling out of connection string setting name', async function (): Promise { + sinon.stub(azureFunctionUtils, 'getAFProjectContainingFile').resolves(vscode.Uri.file('testUri')); + sinon.stub(azureFunctionService, 'getAzureFunctions').withArgs(fileUri.fsPath).returns( + Promise.resolve({ + success: true, + errorMessage: '', + azureFunctions: ['af1'] + })); + + // Mocks promptForConnection + let quickpickStub = sinon.stub(vscode.window, 'showQuickPick'); + // select Azure function + quickpickStub.onFirstCall().resolves({ label: 'af1' }); + // select input or output binding + quickpickStub.onSecondCall().resolves({ label: constants.input, type: BindingType.input }); + + // select connection string setting name + quickpickStub.onThirdCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); + + // give connection string setting name + sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves(undefined); + + 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.getCall(2).args).containDeepOrdered([ + [{ label: constants.createNewLocalAppSettingWithIcon }], + { + canPickMany: false, + title: constants.selectSetting, + ignoreFocusOut: true + }] + ); + }); + + it('Should show user connection string setting method after cancelling out of manually entering connection string', async function (): Promise { + sinon.stub(azureFunctionUtils, 'getAFProjectContainingFile').resolves(vscode.Uri.file('testUri')); + sinon.stub(azureFunctionService, 'getAzureFunctions').withArgs(fileUri.fsPath).returns( + Promise.resolve({ + success: true, + errorMessage: '', + azureFunctions: ['af1'] + })); + + // Mocks promptForConnection + let quickpickStub = sinon.stub(vscode.window, 'showQuickPick'); + // select Azure function + quickpickStub.onFirstCall().resolves({ label: 'af1' }); + // select input or output binding + quickpickStub.onSecondCall().resolves({ label: constants.input, type: BindingType.input }); + + // select connection string setting name + quickpickStub.onThirdCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); + + // give connection string setting name + let inputBox = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('SqlConnectionString'); + + // select enter connection string manually + quickpickStub.onCall(3).resolves({ label: constants.enterConnectionString }); + + // user cancels prompt to enter connection string + inputBox.onSecondCall().resolves(undefined); + + 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.getCall(4).args).containDeepOrdered([ + [constants.connectionProfile, constants.enterConnectionString], + { + canPickMany: false, + title: constants.selectConnectionString, + ignoreFocusOut: true + }] + ); }); }); diff --git a/extensions/sql-bindings/src/test/service/azureFunctionsService.test.ts b/extensions/sql-bindings/src/test/service/azureFunctionsService.test.ts index 854701cd3e..5349df0caf 100644 --- a/extensions/sql-bindings/src/test/service/azureFunctionsService.test.ts +++ b/extensions/sql-bindings/src/test/service/azureFunctionsService.test.ts @@ -3,21 +3,21 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as should from 'should'; -import * as sinon from 'sinon'; -import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; +import * as should from 'should'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import * as utils from '../../common/utils'; -import * as constants from '../../common/constants'; +import * as vscode from 'vscode'; import * as azureFunctionUtils from '../../common/azureFunctionsUtils'; +import * as constants from '../../common/constants'; +import * as utils from '../../common/utils'; import * as azureFunctionsContracts from '../../contracts/azureFunctions/azureFunctionsContracts'; import * as azureFunctionService from '../../services/azureFunctionsService'; -import { createTestUtils, TestUtils, createTestCredentials, createTestTableNode } from '../testUtils'; -import { IConnectionInfo } from 'vscode-mssql'; import { BindingType } from 'sql-bindings'; +import { IConnectionInfo } from 'vscode-mssql'; +import { createTestCredentials, createTestTableNode, createTestUtils, TestUtils } from '../testUtils'; const rootFolderPath = 'test'; const projectFilePath: string = path.join(rootFolderPath, 'test.csproj');