From e79ec552e66326114538de9c6eb8517814f6f0fc Mon Sep 17 00:00:00 2001 From: Sakshi Sharma <57200045+SakshiS-harma@users.noreply.github.com> Date: Mon, 21 Nov 2022 11:04:46 -0800 Subject: [PATCH] Sql projects: Tests for Create project from database for vscode extension (#21257) * Test changes * Tests for CreateProjectFromDatabaseQuickpick * Address comments * Update pwd to placeholder --- .../createProjectFromDatabaseQuickpick.ts | 39 +-- .../src/dialogs/quickpickHelper.ts | 53 ++++ ...createProjectFromDatabaseQuickpick.test.ts | 252 ++++++++++++++++++ .../src/test/dialogs/testUtils.ts | 98 +++++++ 4 files changed, 406 insertions(+), 36 deletions(-) create mode 100644 extensions/sql-database-projects/src/dialogs/quickpickHelper.ts create mode 100644 extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseQuickpick.test.ts create mode 100644 extensions/sql-database-projects/src/test/dialogs/testUtils.ts diff --git a/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseQuickpick.ts b/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseQuickpick.ts index 6b181939f5..5b02f70084 100644 --- a/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseQuickpick.ts +++ b/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseQuickpick.ts @@ -12,12 +12,13 @@ import { defaultProjectNameFromDb, defaultProjectSaveLocation } from '../tools/n import { ImportDataModel } from '../models/api/import'; import { mapExtractTargetEnum } from './createProjectFromDatabaseDialog'; +import { getSDKStyleProjectInfo } from './quickpickHelper'; + /** * Create flow for a New Project using only VS Code-native APIs such as QuickPick * @param connectionInfo Optional connection info to use instead of prompting the user for a connection */ export async function createNewProjectFromDatabaseWithQuickpick(connectionInfo?: IConnectionInfo): Promise { - const vscodeMssqlApi = await getVscodeMssqlApi(); // 1. Select connection @@ -147,41 +148,7 @@ export async function createNewProjectFromDatabaseWithQuickpick(connectionInfo?: const includePermissions = includePermissionsResult === constants.yesString; // 7. SDK-style project or not - let sdkStyle; - const sdkLearnMoreButton: vscode.QuickInputButton = { - iconPath: new vscode.ThemeIcon('link-external'), - tooltip: constants.learnMore - }; - const quickPick = vscode.window.createQuickPick(); - quickPick.items = [{ label: constants.YesRecommended }, { label: constants.noString }]; - quickPick.title = constants.sdkStyleProject; - quickPick.ignoreFocusOut = true; - const disposables: vscode.Disposable[] = []; - - try { - quickPick.buttons = [sdkLearnMoreButton]; - quickPick.placeholder = constants.SdkLearnMorePlaceholder; - - const sdkStylePromise = new Promise((resolve) => { - disposables.push( - quickPick.onDidHide(() => { - resolve(undefined); - }), - quickPick.onDidChangeSelection((item) => { - resolve(item[0].label === constants.YesRecommended); - })); - - disposables.push(quickPick.onDidTriggerButton(async () => { - await vscode.env.openExternal(vscode.Uri.parse(constants.sdkLearnMoreUrl!)); - })); - }); - - quickPick.show(); - sdkStyle = await sdkStylePromise; - quickPick.hide(); - } finally { - disposables.forEach(d => d.dispose()); - } + let sdkStyle = await getSDKStyleProjectInfo(); if (sdkStyle === undefined) { // User cancelled diff --git a/extensions/sql-database-projects/src/dialogs/quickpickHelper.ts b/extensions/sql-database-projects/src/dialogs/quickpickHelper.ts new file mode 100644 index 0000000000..77aad9741c --- /dev/null +++ b/extensions/sql-database-projects/src/dialogs/quickpickHelper.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as constants from '../common/constants'; +import * as vscode from 'vscode'; + +/** + * Function created out of createProjectFromDatabaseQuickpick for testing purposes + * @returns true for sdk style project + * false for legacy style project + * undefined for exiting quickpick + */ +export async function getSDKStyleProjectInfo(): Promise { + let sdkStyle; + const sdkLearnMoreButton: vscode.QuickInputButton = { + iconPath: new vscode.ThemeIcon('link-external'), + tooltip: constants.learnMore + }; + const quickPick = vscode.window.createQuickPick(); + quickPick.items = [{ label: constants.YesRecommended }, { label: constants.noString }]; + quickPick.title = constants.sdkStyleProject; + quickPick.ignoreFocusOut = true; + const disposables: vscode.Disposable[] = []; + + try { + quickPick.buttons = [sdkLearnMoreButton]; + quickPick.placeholder = constants.SdkLearnMorePlaceholder; + + const sdkStylePromise = new Promise((resolve) => { + disposables.push( + quickPick.onDidHide(() => { + resolve(undefined); + }), + quickPick.onDidChangeSelection((item) => { + resolve(item[0].label === constants.YesRecommended); + })); + + disposables.push(quickPick.onDidTriggerButton(async () => { + await vscode.env.openExternal(vscode.Uri.parse(constants.sdkLearnMoreUrl!)); + })); + }); + + quickPick.show(); + sdkStyle = await sdkStylePromise; + quickPick.hide(); + } finally { + disposables.forEach(d => d.dispose()); + } + + return sdkStyle; +} diff --git a/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseQuickpick.test.ts b/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseQuickpick.test.ts new file mode 100644 index 0000000000..fbf613c549 --- /dev/null +++ b/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseQuickpick.test.ts @@ -0,0 +1,252 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as should from 'should'; +import * as mssql from 'mssql'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import * as constants from '../../common/constants'; +import * as utils from '../../common/utils' +import * as quickpickHelper from '../../dialogs/quickpickHelper' +import * as createProjectFromDatabaseQuickpick from '../../dialogs/createProjectFromDatabaseQuickpick'; +import { createTestUtils, mockConnectionInfo, TestUtils } from './testUtils'; +import { promises as fs } from 'fs'; +import { ImportDataModel } from '../../models/api/import'; +import { createTestFile, deleteGeneratedTestFolder } from '../testUtils'; + +let testUtils: TestUtils; +const projectFilePath = 'test'; +const dbList: string[] = constants.systemDbs.concat(['OtherDatabase', 'Database', 'OtherDatabase2']); + +describe('Create Project From Database Quickpick', () => { + beforeEach(function (): void { + testUtils = createTestUtils(); + sinon.stub(utils, 'getVscodeMssqlApi').resolves(testUtils.vscodeMssqlIExtension.object); //set vscode mssql extension api + }); + + afterEach(function (): void { + sinon.restore(); + }); + + it('Should prompt for connection and exit when connection is not selected', async function (): Promise { + //promptForConnection spy to verify test + const promptForConnectionSpy = sinon.stub(testUtils.vscodeMssqlIExtension.object, 'promptForConnection').withArgs(sinon.match.any).resolves(undefined); + + const model = await createProjectFromDatabaseQuickpick.createNewProjectFromDatabaseWithQuickpick(); + + //verify that prompt for connection was called + should(promptForConnectionSpy.calledOnce).be.true('promptForConnection should have been called'); + + //verify quickpick exited with undefined, since promptForConnection was set to cancel (resolves to undefined) + should.equal(model, undefined); + }); + + it('Should not prompt for connection when connectionInfo is provided and exit when db is not selected', async function (): Promise { + //promptForConnection spy to verify test + const promptForConnectionSpy = sinon.stub(testUtils.vscodeMssqlIExtension.object, 'promptForConnection').withArgs(sinon.match.any).resolves(undefined); + + //user chooses connection + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'connect').resolves('testConnectionURI'); + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'listDatabases').withArgs(sinon.match.any).resolves(dbList); + // user chooses to cancel when prompted for database + sinon.stub(vscode.window, 'showQuickPick').resolves(undefined); + + const model = await createProjectFromDatabaseQuickpick.createNewProjectFromDatabaseWithQuickpick(mockConnectionInfo); + + //verify connection prompt wasn't presented, since connectionInfo was passed during the call + should(promptForConnectionSpy.notCalled).be.true('promptForConnection should not be called when connectionInfo is provided'); + + //verify quickpick exited with undefined, since database wasn't selected (resolved to undefined) + should.equal(model, undefined); + }); + + it('Should exit when project name is not selected', async function (): Promise { + //user chooses connection and database + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'connect').resolves('testConnectionURI'); + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'listDatabases').withArgs(sinon.match.any).resolves(dbList); + sinon.stub(vscode.window, 'showQuickPick').resolves('Database' as any); + // user chooses to provide empty project name when prompted + let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').resolves(''); + // user chooses to cancel when prompted to enter project name + inputBoxStub.onSecondCall().resolves(undefined); + + const model = await createProjectFromDatabaseQuickpick.createNewProjectFromDatabaseWithQuickpick(mockConnectionInfo); + + //verify showInputBox exited with undefined, since project name wasn't selected (resolved to undefined) + should.equal(model, undefined); + }); + + it('Should exit when project location is not selected', async function (): Promise { + //user chooses connection and database + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'connect').resolves('testConnectionURI'); + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'listDatabases').withArgs(sinon.match.any).resolves(dbList); + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').resolves('Database' as any); + //user chooses project name + sinon.stub(vscode.window, 'showInputBox').resolves('TestProject'); + //user chooses to exit + quickPickStub.onSecondCall().resolves(undefined); + + const model = await createProjectFromDatabaseQuickpick.createNewProjectFromDatabaseWithQuickpick(mockConnectionInfo); + + //verify showQuickPick exited with undefined, since project location wasn't selected (resolved to undefined) + should.equal(model, undefined); + }); + + it('Should exit when project location is not selected (test repeatedness for project location)', async function (): Promise { + //user chooses connection and database + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'connect').resolves('testConnectionURI'); + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'listDatabases').withArgs(sinon.match.any).resolves(dbList); + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').resolves('Database' as any); + //user chooses project name + sinon.stub(vscode.window, 'showInputBox').resolves('TestProject'); + // user chooses to browse for folder + quickPickStub.onSecondCall().resolves((constants.browseEllipsisWithIcon) as any); + // user doesn't choose any folder when prompted and exits the showOpenDialog + let openDialogStub = sinon.stub(vscode.window, 'showOpenDialog').withArgs(sinon.match.any).resolves(undefined); + // user chooses to browse for folder + quickPickStub.onThirdCall().resolves((constants.browseEllipsisWithIcon) as any); + // user doesn't choose any folder when prompted and exits the showOpenDialog + openDialogStub.onSecondCall().resolves(undefined); + // user chooses to browse for folder + quickPickStub.onCall(3).resolves((constants.browseEllipsisWithIcon) as any); + // user doesn't choose any folder when prompted and exits the showOpenDialog + openDialogStub.onSecondCall().resolves(undefined); + //user chooses to exit + quickPickStub.onCall(4).resolves(undefined); + + const model = await createProjectFromDatabaseQuickpick.createNewProjectFromDatabaseWithQuickpick(mockConnectionInfo); + + //verify showQuickPick exited with undefined, since project location wasn't selected (resolved to undefined) + should.equal(model, undefined); + }); + + it('Should exit when folder structure is not selected and folder is selected through browsing (test repeatedness for project location)', async function (): Promise { + //user chooses connection and database + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'connect').resolves('testConnectionURI'); + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'listDatabases').withArgs(sinon.match.any).resolves(dbList); + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').resolves('Database' as any); + //user chooses project name + sinon.stub(vscode.window, 'showInputBox').resolves('TestProject'); + // user chooses to browse for folder + quickPickStub.onSecondCall().resolves((constants.browseEllipsisWithIcon) as any); + // user doesn't choose any folder when prompted and exits the showOpenDialog + let openDialogStub = sinon.stub(vscode.window, 'showOpenDialog').withArgs(sinon.match.any).resolves(undefined); + // user chooses to browse for folder again + quickPickStub.onThirdCall().resolves((constants.browseEllipsisWithIcon) as any); + // user chooses folder- stub out folder to be chosen (showOpenDialog) + openDialogStub.onSecondCall().resolves([vscode.Uri.file(projectFilePath)]); + //user chooses to exit when prompted for folder structure + quickPickStub.onCall(3).resolves(undefined); + + const model = await createProjectFromDatabaseQuickpick.createNewProjectFromDatabaseWithQuickpick(mockConnectionInfo); + + //verify showQuickPick exited with undefined, since folder structure wasn't selected (resolved to undefined) + should.equal(model, undefined); + }); + + it('Should exit when folder structure is not selected and existing folder/file location is selected', async function (): Promise { + //create folder and project file + const projectFileName = 'TestProject'; + const testProjectFilePath = 'TestProjectPath' + await fs.rm(testProjectFilePath, { force: true, recursive: true }); //clean up if it already exists + await createTestFile('', projectFileName, testProjectFilePath); + + //user chooses connection and database + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'connect').resolves('testConnectionURI'); + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'listDatabases').withArgs(sinon.match.any).resolves(dbList); + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').resolves('Database' as any); + //user chooses project name + sinon.stub(vscode.window, 'showInputBox').resolves(projectFileName); + // user chooses a folder/file combination that already exists + quickPickStub.onSecondCall().resolves(testProjectFilePath as any); + //user chooses another folder when prompted again + quickPickStub.onThirdCall().resolves(path.join(projectFilePath, 'test') as any); + //user chooses to exit when prompted for folder structure + quickPickStub.onCall(3).resolves(undefined); + + const model = await createProjectFromDatabaseQuickpick.createNewProjectFromDatabaseWithQuickpick(mockConnectionInfo); + + await deleteGeneratedTestFolder(); + + //verify showQuickPick exited with undefined, since folder structure wasn't selected (resolved to undefined) + should.equal(model, undefined); + }); + + it('Should exit when include permissions is not selected', async function (): Promise { + //user chooses connection and database + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'connect').resolves('testConnectionURI'); + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'listDatabases').withArgs(sinon.match.any).resolves(dbList); + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').resolves('Database' as any); + //user chooses project name + sinon.stub(vscode.window, 'showInputBox').resolves('TestProject'); + // user chooses a folder + quickPickStub.onSecondCall().resolves(projectFilePath as any); + //user chooses Object type when prompted for folder structure + quickPickStub.onThirdCall().resolves(constants.objectType as any); + //user chooses to exit when prompted for include permissions + quickPickStub.onCall(3).resolves(undefined); + + const model = await createProjectFromDatabaseQuickpick.createNewProjectFromDatabaseWithQuickpick(mockConnectionInfo); + + //verify showQuickPick exited with undefined, since include permissions wasn't selected (resolved to undefined) + should.equal(model, undefined); + }); + + it('Should exit when sdk style project is not selected', async function (): Promise { + //user chooses connection and database + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'connect').resolves('testConnectionURI'); + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'listDatabases').withArgs(sinon.match.any).resolves(dbList); + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').resolves('Database' as any); + //user chooses project name + sinon.stub(vscode.window, 'showInputBox').resolves('TestProject'); + // user chooses a folder + quickPickStub.onSecondCall().resolves(projectFilePath as any); + //user chooses Object type when prompted for folder structure + quickPickStub.onThirdCall().resolves(constants.objectType as any); + //user chooses No when prompted for include permissions + quickPickStub.onCall(3).resolves(constants.noStringDefault as any); + //user chooses to exit when prompted for sdk style project + sinon.stub(quickpickHelper, 'getSDKStyleProjectInfo').resolves(undefined); + + const model = await createProjectFromDatabaseQuickpick.createNewProjectFromDatabaseWithQuickpick(mockConnectionInfo); + + //verify showQuickPick exited with undefined, since sdk style project wasn't selected (resolved to undefined) + should.equal(model, undefined); + }); + + it('Should create correct import data model when all the information is provided', async function (): Promise { + //user chooses connection and database + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'connect').resolves('testConnectionURI'); + sinon.stub(testUtils.vscodeMssqlIExtension.object, 'listDatabases').withArgs(sinon.match.any).resolves(dbList); + let quickPickStub = sinon.stub(vscode.window, 'showQuickPick').resolves('Database' as any); + //user chooses project name + sinon.stub(vscode.window, 'showInputBox').resolves('TestProject'); + // user chooses a folder + quickPickStub.onSecondCall().resolves(projectFilePath as any); + //user chooses Object type when prompted for folder structure + quickPickStub.onThirdCall().resolves(constants.objectType as any); + //user chooses No when prompted for include permissions + quickPickStub.onCall(3).resolves(constants.noStringDefault as any); + //user chooses sdk style project to be true + sinon.stub(quickpickHelper, 'getSDKStyleProjectInfo').resolves(true); + + const model = await createProjectFromDatabaseQuickpick.createNewProjectFromDatabaseWithQuickpick(mockConnectionInfo); + + const expectedImportDataModel: ImportDataModel = { + connectionUri: 'testConnectionURI', + database: 'Database', + projName: 'TestProject', + filePath: projectFilePath, + version: '1.0.0.0', + extractTarget: mssql.ExtractTarget.objectType, + sdkStyle: true, + includePermissions: false + }; + + //verify the model is correctly generated + should(model!).deepEqual(expectedImportDataModel); + }); +}); diff --git a/extensions/sql-database-projects/src/test/dialogs/testUtils.ts b/extensions/sql-database-projects/src/test/dialogs/testUtils.ts new file mode 100644 index 0000000000..1bbd62e1e6 --- /dev/null +++ b/extensions/sql-database-projects/src/test/dialogs/testUtils.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as TypeMoq from 'typemoq'; +import * as vscodeMssql from 'vscode-mssql'; +import { RequestType } from 'vscode-languageclient'; + +export interface TestUtils { + vscodeMssqlIExtension: TypeMoq.IMock; +} + +export class MockVscodeMssqlIExtension implements vscodeMssql.IExtension { + sqlToolsServicePath: string = ''; + dacFx: vscodeMssql.IDacFxService; + schemaCompare: vscodeMssql.ISchemaCompareService; + azureAccountService: vscodeMssql.IAzureAccountService; + azureResourceService: vscodeMssql.IAzureResourceService; + + constructor() { + this.dacFx = TypeMoq.Mock.ofType().object; + this.schemaCompare = TypeMoq.Mock.ofType().object; + this.azureAccountService = TypeMoq.Mock.ofType().object; + this.azureResourceService = TypeMoq.Mock.ofType().object; + } + + promptForFirewallRule(_: string, __: vscodeMssql.IConnectionInfo): Promise { + throw new Error('Method not implemented.'); + } + sendRequest(_: RequestType, __?: P): Promise { + throw new Error('Method not implemented.'); + } + promptForConnection(_?: boolean): Promise { + throw new Error('Method not implemented.'); + } + connect(_: vscodeMssql.IConnectionInfo, __?: boolean): Promise { + throw new Error('Method not implemented.'); + } + listDatabases(_: string): Promise { + throw new Error('Method not implemented.'); + } + getDatabaseNameFromTreeNode(_: vscodeMssql.ITreeNodeInfo): string { + throw new Error('Method not implemented.'); + } + getConnectionString(_: string | vscodeMssql.ConnectionDetails, ___?: boolean, _____?: boolean): Promise { + throw new Error('Method not implemented.'); + } + createConnectionDetails(_: vscodeMssql.IConnectionInfo): vscodeMssql.ConnectionDetails { + throw new Error('Method not implemented.'); + } + getServerInfo(_: vscodeMssql.IConnectionInfo): vscodeMssql.ServerInfo { + throw new Error('Method not implemented.'); + } +} + +export function createTestUtils(): TestUtils { + return { + vscodeMssqlIExtension: TypeMoq.Mock.ofType(MockVscodeMssqlIExtension) + }; +} + +// Mock test data +export const mockConnectionInfo: vscodeMssql.IConnectionInfo = { + server: 'Server', + database: 'Database', + user: 'User', + password: 'Placeholder', + email: 'test-email', + accountId: 'test-account-id', + tenantId: 'test-tenant-id', + port: 1234, + authenticationType: vscodeMssql.AuthenticationType.SqlLogin, + azureAccountToken: '', + expiresOn: 0, + encrypt: false, + trustServerCertificate: false, + persistSecurityInfo: false, + connectTimeout: 15, + connectRetryCount: 0, + connectRetryInterval: 0, + applicationName: 'vscode-mssql', + workstationId: 'test', + applicationIntent: '', + currentLanguage: '', + pooling: true, + maxPoolSize: 15, + minPoolSize: 0, + loadBalanceTimeout: 0, + replication: false, + attachDbFilename: '', + failoverPartner: '', + multiSubnetFailover: false, + multipleActiveResultSets: false, + packetSize: 8192, + typeSystemVersion: 'Latest', + connectionString: '' +};