diff --git a/extensions/sql-database-projects/package.json b/extensions/sql-database-projects/package.json index b532736bdb..c06e1a2a5e 100644 --- a/extensions/sql-database-projects/package.json +++ b/extensions/sql-database-projects/package.json @@ -315,10 +315,12 @@ "xmldom": "^0.3.0" }, "devDependencies": { + "@types/sinon": "^9.0.4", "@types/xmldom": "^0.1.29", "mocha-junit-reporter": "^1.17.0", "mocha-multi-reporters": "^1.1.7", "should": "^13.2.1", + "sinon": "^9.0.2", "tslint": "^5.8.0", "typemoq": "^2.1.0", "typescript": "^2.6.1", diff --git a/extensions/sql-database-projects/src/common/apiWrapper.ts b/extensions/sql-database-projects/src/common/apiWrapper.ts deleted file mode 100644 index a242b2446c..0000000000 --- a/extensions/sql-database-projects/src/common/apiWrapper.ts +++ /dev/null @@ -1,227 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 azdata from 'azdata'; - -/** - * Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into - * this API from our code - */ -export class ApiWrapper { - //#region azdata.accounts - - public getAllAccounts(): Thenable { - return azdata.accounts.getAllAccounts(); - } - - public getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Thenable<{ [key: string]: any }> { - return azdata.accounts.getSecurityToken(account, resource); - } - - //#endregion - - //#region azdata.connection - - public getCurrentConnection(): Thenable { - return azdata.connection.getCurrentConnection(); - } - - public openConnectionDialog(providers?: string[], - initialConnectionProfile?: azdata.IConnectionProfile, - connectionCompletionOptions?: azdata.IConnectionCompletionOptions): Thenable { - return azdata.connection.openConnectionDialog(providers, initialConnectionProfile, connectionCompletionOptions); - } - - public getCredentials(connectionId: string): Thenable<{ [name: string]: string }> { - return azdata.connection.getCredentials(connectionId); - } - - public connectionConnect(connectionProfile: azdata.IConnectionProfile, saveConnection?: boolean, showDashboard?: boolean): Thenable { - return azdata.connection.connect(connectionProfile, saveConnection, showDashboard); - } - - public getUriForConnection(connectionId: string): Thenable { - return azdata.connection.getUriForConnection(connectionId); - } - - public listDatabases(connectionId: string): Thenable { - return azdata.connection.listDatabases(connectionId); - } - - //#endregion - - //#region azdata.dataprotocol - - public getProvider(providerId: string, providerType: azdata.DataProviderType): T { - return azdata.dataprotocol.getProvider(providerId, providerType); - } - - //#endregion - - //#region azdata.queryeditor - - public connect(fileUri: string, connectionId: string): Thenable { - return azdata.queryeditor.connect(fileUri, connectionId); - } - - public runQuery(fileUri: string, options?: Map, runCurrentQuery?: boolean): void { - azdata.queryeditor.runQuery(fileUri, options, runCurrentQuery); - } - - //#endregion - - //#region azdata.tasks - - public registerTaskHandler(taskId: string, handler: (profile: azdata.IConnectionProfile) => void): void { - azdata.tasks.registerTask(taskId, handler); - } - - public startBackgroundOperation(operationInfo: azdata.BackgroundOperationInfo): void { - azdata.tasks.startBackgroundOperation(operationInfo); - } - - //#endregion - - //#region azdata.ui - - public registerWidget(widgetId: string, handler: (view: azdata.ModelView) => void): void { - azdata.ui.registerModelViewProvider(widgetId, handler); - } - - //#endregion - - //#region azdata.window - - public closeDialog(dialog: azdata.window.Dialog) { - azdata.window.closeDialog(dialog); - } - - //#endregion - - //#region vscode.commands - - public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable { - return vscode.commands.registerCommand(command, callback, thisArg); - } - - public executeCommand(command: string, ...rest: any[]): Thenable { - return vscode.commands.executeCommand(command, ...rest); - } - - //#endregion - - //#region vscode.env - - public openExternal(target: vscode.Uri): Thenable { - return vscode.env.openExternal(target); - } - - //#endregion - - //#region vscode.extensions - - public getExtension(extensionId: string): vscode.Extension | undefined { - return vscode.extensions.getExtension(extensionId); - } - - //#endregion - - //#region vscode.window - - public createOutputChannel(name: string): vscode.OutputChannel { - return vscode.window.createOutputChannel(name); - } - - public createTerminalWithOptions(options: vscode.TerminalOptions): vscode.Terminal { - return vscode.window.createTerminal(options); - } - - public registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { - return vscode.window.registerTreeDataProvider(viewId, treeDataProvider); - } - - public showErrorMessage(message: string, ...items: string[]): Thenable { - return vscode.window.showErrorMessage(message, ...items); - } - - public showInformationMessage(message: string, ...items: string[]): Thenable { - return vscode.window.showInformationMessage(message, ...items); - } - - public showWarningMessage(message: string, ...items: string[]): Thenable { - return vscode.window.showWarningMessage(message, ...items); - } - - public showWarningMessageOptions(message: string, options: vscode.MessageOptions, ...items: string[]): Thenable { - return vscode.window.showWarningMessage(message, options, ...items); - } - - public showOpenDialog(options: vscode.OpenDialogOptions): Thenable { - return vscode.window.showOpenDialog(options); - } - - public createTab(title: string): azdata.window.DialogTab { - return azdata.window.createTab(title); - } - - public createModelViewDialog(title: string, dialogName?: string, isWide?: boolean): azdata.window.Dialog { - return azdata.window.createModelViewDialog(title, dialogName, isWide); - } - - public createWizard(title: string): azdata.window.Wizard { - return azdata.window.createWizard(title); - } - - public createWizardPage(title: string): azdata.window.WizardPage { - return azdata.window.createWizardPage(title); - } - - public openDialog(dialog: azdata.window.Dialog): void { - return azdata.window.openDialog(dialog); - } - - public showQuickPick(items: T[] | Thenable, options?: vscode.QuickPickOptions, token?: vscode.CancellationToken): Thenable { - return vscode.window.showQuickPick(items, options, token); - } - - public showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken): Thenable { - return vscode.window.showInputBox(options, token); - } - - public showSaveDialog(options: vscode.SaveDialogOptions): Thenable { - return vscode.window.showSaveDialog(options); - } - - public showTextDocument(uri: vscode.Uri, options?: vscode.TextDocumentShowOptions): Thenable { - return vscode.window.showTextDocument(uri, options); - } - - public createButton(label: string, position?: azdata.window.DialogButtonPosition): azdata.window.Button { - return azdata.window.createButton(label, position); - } - - public createTreeView(viewId: string, options: vscode.TreeViewOptions): vscode.TreeView { - return vscode.window.createTreeView(viewId, options); - } - - //#endregion - - //#region vscode.workspace - - public getConfiguration(section?: string, resource?: vscode.Uri | null): vscode.WorkspaceConfiguration { - return vscode.workspace.getConfiguration(section, resource); - } - - public workspaceFolders(): readonly vscode.WorkspaceFolder[] | undefined { - return vscode.workspace.workspaceFolders; - } - - public openTextDocument(options?: { language?: string; content?: string; }): Thenable { - return vscode.workspace.openTextDocument(options); - } - - //#endregion -} diff --git a/extensions/sql-database-projects/src/controllers/mainController.ts b/extensions/sql-database-projects/src/controllers/mainController.ts index 22d5c4192e..1f364828d9 100644 --- a/extensions/sql-database-projects/src/controllers/mainController.ts +++ b/extensions/sql-database-projects/src/controllers/mainController.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import * as vscode from 'vscode'; import * as templates from '../templates/templates'; import * as constants from '../common/constants'; import * as path from 'path'; import * as glob from 'fast-glob'; -import { Uri, Disposable, ExtensionContext, WorkspaceFolder } from 'vscode'; -import { ApiWrapper } from '../common/apiWrapper'; import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider'; import { getErrorMessage } from '../common/utils'; import { ProjectsController } from './projectController'; @@ -24,17 +23,17 @@ const SQL_DATABASE_PROJECTS_VIEW_ID = 'sqlDatabaseProjectsView'; /** * The main controller class that initializes the extension */ -export default class MainController implements Disposable { +export default class MainController implements vscode.Disposable { protected dbProjectTreeViewProvider: SqlDatabaseProjectTreeViewProvider = new SqlDatabaseProjectTreeViewProvider(); protected projectsController: ProjectsController; protected netcoreTool: NetCoreTool; - public constructor(private context: ExtensionContext, private apiWrapper: ApiWrapper) { - this.projectsController = new ProjectsController(apiWrapper, this.dbProjectTreeViewProvider); + public constructor(private context: vscode.ExtensionContext) { + this.projectsController = new ProjectsController(this.dbProjectTreeViewProvider); this.netcoreTool = new NetCoreTool(); } - public get extensionContext(): ExtensionContext { + public get extensionContext(): vscode.ExtensionContext { return this.context; } @@ -51,30 +50,30 @@ export default class MainController implements Disposable { private async initializeDatabaseProjects(): Promise { // init commands - this.apiWrapper.registerCommand('sqlDatabaseProjects.new', async () => { await this.createNewProject(); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.open', async () => { await this.openProjectFromFile(); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.close', (node: BaseProjectTreeItem) => { this.projectsController.closeProject(node); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.properties', async (node: BaseProjectTreeItem) => { await this.apiWrapper.showErrorMessage(`Properties not yet implemented: ${node.uri.path}`); }); // TODO + vscode.commands.registerCommand('sqlDatabaseProjects.new', async () => { await this.createNewProject(); }); + vscode.commands.registerCommand('sqlDatabaseProjects.open', async () => { await this.openProjectFromFile(); }); + vscode.commands.registerCommand('sqlDatabaseProjects.close', (node: BaseProjectTreeItem) => { this.projectsController.closeProject(node); }); + vscode.commands.registerCommand('sqlDatabaseProjects.properties', async (node: BaseProjectTreeItem) => { await vscode.window.showErrorMessage(`Properties not yet implemented: ${node.uri.path}`); }); // TODO - this.apiWrapper.registerCommand('sqlDatabaseProjects.build', async (node: BaseProjectTreeItem) => { await this.projectsController.buildProject(node); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.publish', async (node: BaseProjectTreeItem) => { await this.projectsController.publishProject(node); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.schemaCompare', async (node: BaseProjectTreeItem) => { await this.projectsController.schemaCompare(node); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.importDatabase', async (profile: azdata.IConnectionProfile) => { await this.projectsController.importNewDatabaseProject(profile); }); + vscode.commands.registerCommand('sqlDatabaseProjects.build', async (node: BaseProjectTreeItem) => { await this.projectsController.buildProject(node); }); + vscode.commands.registerCommand('sqlDatabaseProjects.publish', async (node: BaseProjectTreeItem) => { await this.projectsController.publishProject(node); }); + vscode.commands.registerCommand('sqlDatabaseProjects.schemaCompare', async (node: BaseProjectTreeItem) => { await this.projectsController.schemaCompare(node); }); + vscode.commands.registerCommand('sqlDatabaseProjects.importDatabase', async (profile: azdata.IConnectionProfile) => { await this.projectsController.importNewDatabaseProject(profile); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.newScript', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.script); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.newTable', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.table); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.newView', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.view); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.newStoredProcedure', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.storedProcedure); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.newItem', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.newFolder', async (node: BaseProjectTreeItem) => { await this.projectsController.addFolderPrompt(node); }); + vscode.commands.registerCommand('sqlDatabaseProjects.newScript', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.script); }); + vscode.commands.registerCommand('sqlDatabaseProjects.newTable', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.table); }); + vscode.commands.registerCommand('sqlDatabaseProjects.newView', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.view); }); + vscode.commands.registerCommand('sqlDatabaseProjects.newStoredProcedure', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.storedProcedure); }); + vscode.commands.registerCommand('sqlDatabaseProjects.newItem', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node); }); + vscode.commands.registerCommand('sqlDatabaseProjects.newFolder', async (node: BaseProjectTreeItem) => { await this.projectsController.addFolderPrompt(node); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.addDatabaseReference', async (node: BaseProjectTreeItem) => { await this.projectsController.addDatabaseReference(node); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.openContainingFolder', async (node: BaseProjectTreeItem) => { await this.projectsController.openContainingFolder(node); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.delete', async (node: BaseProjectTreeItem) => { await this.projectsController.delete(node); }); - this.apiWrapper.registerCommand('sqlDatabaseProjects.exclude', async (node: FileNode | FolderNode) => { await this.projectsController.exclude(node); }); + vscode.commands.registerCommand('sqlDatabaseProjects.addDatabaseReference', async (node: BaseProjectTreeItem) => { await this.projectsController.addDatabaseReference(node); }); + vscode.commands.registerCommand('sqlDatabaseProjects.openContainingFolder', async (node: BaseProjectTreeItem) => { await this.projectsController.openContainingFolder(node); }); + vscode.commands.registerCommand('sqlDatabaseProjects.delete', async (node: BaseProjectTreeItem) => { await this.projectsController.delete(node); }); + vscode.commands.registerCommand('sqlDatabaseProjects.exclude', async (node: FileNode | FolderNode) => { await this.projectsController.exclude(node); }); // init view - const treeView = this.apiWrapper.createTreeView(SQL_DATABASE_PROJECTS_VIEW_ID, { treeDataProvider: this.dbProjectTreeViewProvider }); + const treeView = vscode.window.createTreeView(SQL_DATABASE_PROJECTS_VIEW_ID, { treeDataProvider: this.dbProjectTreeViewProvider }); this.dbProjectTreeViewProvider.setTreeView(treeView); this.extensionContext.subscriptions.push(treeView); @@ -89,7 +88,7 @@ export default class MainController implements Disposable { } public async loadProjectsInWorkspace(): Promise { - const workspaceFolders = this.apiWrapper.workspaceFolders(); + const workspaceFolders = vscode.workspace.workspaceFolders; if (workspaceFolders?.length) { await Promise.all(workspaceFolders.map(async (workspaceFolder) => { await this.loadProjectsInFolder(workspaceFolder.uri.fsPath); @@ -104,7 +103,7 @@ export default class MainController implements Disposable { let results = await glob(sqlprojFilter); for (let f in results) { - await this.projectsController.openProject(Uri.file(results[f])); + await this.projectsController.openProject(vscode.Uri.file(results[f])); } } @@ -118,7 +117,7 @@ export default class MainController implements Disposable { filter[constants.sqlDatabaseProject] = ['sqlproj']; - let files: Uri[] | undefined = await this.apiWrapper.showOpenDialog({ filters: filter }); + let files: vscode.Uri[] | undefined = await vscode.window.showOpenDialog({ filters: filter }); if (files) { for (const file of files) { @@ -127,7 +126,7 @@ export default class MainController implements Disposable { } } catch (err) { - this.apiWrapper.showErrorMessage(getErrorMessage(err)); + vscode.window.showErrorMessage(getErrorMessage(err)); } } @@ -136,7 +135,7 @@ export default class MainController implements Disposable { */ public async createNewProject(): Promise { try { - let newProjName = await this.apiWrapper.showInputBox({ + let newProjName = await vscode.window.showInputBox({ prompt: constants.newDatabaseProjectName, value: `DatabaseProject${this.projectsController.projects.length + 1}` // TODO: Smarter way to suggest a name. Easy if we prompt for location first, but that feels odd... @@ -146,32 +145,32 @@ export default class MainController implements Disposable { if (!newProjName) { // TODO: is this case considered an intentional cancellation (shouldn't warn) or an error case (should warn)? - this.apiWrapper.showErrorMessage(constants.projectNameRequired); + vscode.window.showErrorMessage(constants.projectNameRequired); return undefined; } - let selectionResult = await this.apiWrapper.showOpenDialog({ + let selectionResult = await vscode.window.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, - defaultUri: this.apiWrapper.workspaceFolders() ? (this.apiWrapper.workspaceFolders() as WorkspaceFolder[])[0].uri : undefined + defaultUri: vscode.workspace.workspaceFolders ? (vscode.workspace.workspaceFolders as vscode.WorkspaceFolder[])[0].uri : undefined }); if (!selectionResult) { - this.apiWrapper.showErrorMessage(constants.projectLocationRequired); + vscode.window.showErrorMessage(constants.projectLocationRequired); return undefined; } // TODO: what if the selected folder is outside the workspace? - const newProjFolderUri = (selectionResult as Uri[])[0]; + const newProjFolderUri = (selectionResult as vscode.Uri[])[0]; const newProjFilePath = await this.projectsController.createNewProject(newProjName, newProjFolderUri, true); - const proj = await this.projectsController.openProject(Uri.file(newProjFilePath)); + const proj = await this.projectsController.openProject(vscode.Uri.file(newProjFilePath)); return proj; } catch (err) { - this.apiWrapper.showErrorMessage(getErrorMessage(err)); + vscode.window.showErrorMessage(getErrorMessage(err)); return undefined; } } diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 43688b14ef..a779395aeb 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -13,10 +13,9 @@ import * as UUID from 'vscode-languageclient/lib/utils/uuid'; import * as templates from '../templates/templates'; import * as xmldom from 'xmldom'; -import { Uri, QuickPickItem, WorkspaceFolder, extensions, Extension } from 'vscode'; -import { IConnectionProfile, TaskExecutionMode } from 'azdata'; +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; import { promises as fs } from 'fs'; -import { ApiWrapper } from '../common/apiWrapper'; import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog'; import { Project, DatabaseReferenceLocation, SystemDatabase, TargetPlatform, ProjectEntry, reservedProjectFolders } from '../models/project'; import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider'; @@ -38,7 +37,7 @@ export class ProjectsController { projects: Project[] = []; - constructor(private apiWrapper: ApiWrapper, projTreeViewProvider: SqlDatabaseProjectTreeViewProvider) { + constructor(projTreeViewProvider: SqlDatabaseProjectTreeViewProvider) { this.projectTreeViewProvider = projTreeViewProvider; this.netCoreTool = new NetCoreTool(); this.buildHelper = new BuildHelper(); @@ -48,10 +47,10 @@ export class ProjectsController { this.projectTreeViewProvider.load(this.projects); } - public async openProject(projectFile: Uri): Promise { + public async openProject(projectFile: vscode.Uri): Promise { for (const proj of this.projects) { if (proj.projectFilePath === projectFile.fsPath) { - this.apiWrapper.showInformationMessage(constants.projectAlreadyOpened(projectFile.fsPath)); + vscode.window.showInformationMessage(constants.projectAlreadyOpened(projectFile.fsPath)); return proj; } } @@ -96,7 +95,7 @@ export class ProjectsController { public async focusProject(project?: Project): Promise { if (project && this.projects.includes(project)) { - await this.apiWrapper.executeCommand(constants.sqlDatabaseProjectsViewFocusCommand); + await vscode.commands.executeCommand(constants.sqlDatabaseProjectsViewFocusCommand); await this.projectTreeViewProvider.focus(project); } } @@ -107,7 +106,7 @@ export class ProjectsController { * @param folderUri * @param projectGuid */ - public async createNewProject(newProjName: string, folderUri: Uri, makeOwnFolder: boolean, projectGuid?: string): Promise { + public async createNewProject(newProjName: string, folderUri: vscode.Uri, makeOwnFolder: boolean, projectGuid?: string): Promise { if (projectGuid && !UUID.isUUID(projectGuid)) { throw new Error(`Specified GUID is invalid: '${projectGuid}'`); } @@ -179,7 +178,7 @@ export class ProjectsController { return path.join(project.projectFolderPath, 'bin', 'Debug', `${project.projectFileName}.dacpac`); } catch (err) { - this.apiWrapper.showErrorMessage(constants.projBuildFailed(utils.getErrorMessage(err))); + vscode.window.showErrorMessage(constants.projBuildFailed(utils.getErrorMessage(err))); return undefined; } } @@ -221,14 +220,14 @@ export class ProjectsController { const dacFxService = await this.getDaxFxService(); if ((settings).upgradeExisting) { - return await dacFxService.deployDacpac(tempPath, settings.databaseName, (settings).upgradeExisting, settings.connectionUri, TaskExecutionMode.execute, settings.sqlCmdVariables); + return await dacFxService.deployDacpac(tempPath, settings.databaseName, (settings).upgradeExisting, settings.connectionUri, azdata.TaskExecutionMode.execute, settings.sqlCmdVariables); } else { - return await dacFxService.generateDeployScript(tempPath, settings.databaseName, settings.connectionUri, TaskExecutionMode.script, settings.sqlCmdVariables); + return await dacFxService.generateDeployScript(tempPath, settings.databaseName, settings.connectionUri, azdata.TaskExecutionMode.script, settings.sqlCmdVariables); } } - public async readPublishProfile(profileUri: Uri): Promise { + public async readPublishProfile(profileUri: vscode.Uri): Promise { const profileText = await fs.readFile(profileUri.fsPath); const profileXmlDoc = new xmldom.DOMParser().parseFromString(profileText.toString()); @@ -251,7 +250,7 @@ export class ProjectsController { public async schemaCompare(treeNode: BaseProjectTreeItem): Promise { // check if schema compare extension is installed - if (this.apiWrapper.getExtension(constants.schemaCompareExtensionId)) { + if (vscode.extensions.getExtension(constants.schemaCompareExtensionId)) { // build project await this.buildProject(treeNode); @@ -261,12 +260,12 @@ export class ProjectsController { // check that dacpac exists if (await utils.exists(dacpacPath)) { - await this.apiWrapper.executeCommand(constants.schemaCompareStartCommand, dacpacPath); + await vscode.commands.executeCommand(constants.schemaCompareStartCommand, dacpacPath); } else { - this.apiWrapper.showErrorMessage(constants.buildDacpacNotFound); + vscode.window.showErrorMessage(constants.buildDacpacNotFound); } } else { - this.apiWrapper.showErrorMessage(constants.schemaCompareNotInstalled); + vscode.window.showErrorMessage(constants.schemaCompareNotInstalled); } } @@ -292,7 +291,7 @@ export class ProjectsController { await project.addFolderItem(relativeFolderPath); this.refreshProjectsTree(); } catch (err) { - this.apiWrapper.showErrorMessage(utils.getErrorMessage(err)); + vscode.window.showErrorMessage(utils.getErrorMessage(err)); } } @@ -308,13 +307,13 @@ export class ProjectsController { public async addItemPrompt(project: Project, relativePath: string, itemTypeName?: string) { if (!itemTypeName) { - const items: QuickPickItem[] = []; + const items: vscode.QuickPickItem[] = []; for (const itemType of templates.projectScriptTypes()) { items.push({ label: itemType.friendlyName }); } - itemTypeName = (await this.apiWrapper.showQuickPick(items, { + itemTypeName = (await vscode.window.showQuickPick(items, { canPickMany: false }))?.label; @@ -346,11 +345,11 @@ export class ProjectsController { const newEntry = await project.addScriptItem(relativeFilePath, newFileText); - await this.apiWrapper.executeCommand(constants.vscodeOpenCommand, newEntry.fsUri); + await vscode.commands.executeCommand(constants.vscodeOpenCommand, newEntry.fsUri); this.refreshProjectsTree(); } catch (err) { - this.apiWrapper.showErrorMessage(utils.getErrorMessage(err)); + vscode.window.showErrorMessage(utils.getErrorMessage(err)); } } @@ -362,7 +361,7 @@ export class ProjectsController { if (fileEntry) { await project.exclude(fileEntry); } else { - this.apiWrapper.showErrorMessage(constants.unableToPerformAction(constants.excludeAction, context.uri.path)); + vscode.window.showErrorMessage(constants.unableToPerformAction(constants.excludeAction, context.uri.path)); } this.refreshProjectsTree(); @@ -372,7 +371,7 @@ export class ProjectsController { const project = this.getProjectFromContext(context); const confirmationPrompt = context instanceof FolderNode ? constants.deleteConfirmationContents(context.friendlyName) : constants.deleteConfirmation(context.friendlyName); - const response = await this.apiWrapper.showWarningMessageOptions(confirmationPrompt, { modal: true }, constants.yesString); + const response = await vscode.window.showWarningMessage(confirmationPrompt, { modal: true }, constants.yesString); if (response !== constants.yesString) { return; @@ -392,7 +391,7 @@ export class ProjectsController { if (success) { this.refreshProjectsTree(); } else { - this.apiWrapper.showErrorMessage(constants.unableToPerformAction(constants.deleteAction, context.uri.path)); + vscode.window.showErrorMessage(constants.unableToPerformAction(constants.deleteAction, context.uri.path)); } } @@ -406,7 +405,7 @@ export class ProjectsController { */ public async openContainingFolder(context: BaseProjectTreeItem): Promise { const project = this.getProjectFromContext(context); - await this.apiWrapper.executeCommand(constants.revealFileInOsCommand, Uri.file(project.projectFilePath)); + await vscode.commands.executeCommand(constants.revealFileInOsCommand, vscode.Uri.file(project.projectFilePath)); } /** @@ -439,12 +438,12 @@ export class ProjectsController { this.refreshProjectsTree(); } catch (err) { - this.apiWrapper.showErrorMessage(utils.getErrorMessage(err)); + vscode.window.showErrorMessage(utils.getErrorMessage(err)); } } private async getDatabaseReferenceType(): Promise { - let databaseReferenceOptions: QuickPickItem[] = [ + let databaseReferenceOptions: vscode.QuickPickItem[] = [ { label: constants.systemDatabase }, @@ -453,7 +452,7 @@ export class ProjectsController { } ]; - let input = await this.apiWrapper.showQuickPick(databaseReferenceOptions, { + let input = await vscode.window.showQuickPick(databaseReferenceOptions, { canPickMany: false, placeHolder: constants.addDatabaseReferenceInput }); @@ -466,7 +465,7 @@ export class ProjectsController { } public async getSystemDatabaseName(project: Project): Promise { - let databaseReferenceOptions: QuickPickItem[] = [ + let databaseReferenceOptions: vscode.QuickPickItem[] = [ { label: constants.master } @@ -480,7 +479,7 @@ export class ProjectsController { }); } - let input = await this.apiWrapper.showQuickPick(databaseReferenceOptions, { + let input = await vscode.window.showQuickPick(databaseReferenceOptions, { canPickMany: false, placeHolder: constants.systemDatabaseReferenceInput }); @@ -492,13 +491,13 @@ export class ProjectsController { return input.label === constants.master ? SystemDatabase.master : SystemDatabase.msdb; } - private async getDacpacFileLocation(): Promise { - let fileUris = await this.apiWrapper.showOpenDialog( + private async getDacpacFileLocation(): Promise { + let fileUris = await vscode.window.showOpenDialog( { canSelectFiles: true, canSelectFolders: false, canSelectMany: false, - defaultUri: this.apiWrapper.workspaceFolders() ? (this.apiWrapper.workspaceFolders() as WorkspaceFolder[])[0].uri : undefined, + defaultUri: vscode.workspace.workspaceFolders ? (vscode.workspace.workspaceFolders as vscode.WorkspaceFolder[])[0].uri : undefined, openLabel: constants.selectString, filters: { [constants.dacpacFiles]: ['dacpac'], @@ -514,7 +513,7 @@ export class ProjectsController { } private async getDatabaseLocation(): Promise { - let databaseReferenceOptions: QuickPickItem[] = [ + let databaseReferenceOptions: vscode.QuickPickItem[] = [ { label: constants.databaseReferenceSameDatabase }, @@ -523,7 +522,7 @@ export class ProjectsController { } ]; - let input = await this.apiWrapper.showQuickPick(databaseReferenceOptions, { + let input = await vscode.window.showQuickPick(databaseReferenceOptions, { canPickMany: false, placeHolder: constants.databaseReferenceLocation }); @@ -536,9 +535,9 @@ export class ProjectsController { return location; } - private async getDatabaseName(dacpac: Uri): Promise { + private async getDatabaseName(dacpac: vscode.Uri): Promise { const dacpacName = path.parse(dacpac.toString()).name; - let databaseName = await this.apiWrapper.showInputBox({ + let databaseName = await vscode.window.showInputBox({ prompt: constants.databaseReferenceDatabaseName, value: `${dacpacName}` }); @@ -554,7 +553,7 @@ export class ProjectsController { //#region Helper methods public getPublishDialog(project: Project): PublishDatabaseDialog { - return new PublishDatabaseDialog(this.apiWrapper, project); + return new PublishDatabaseDialog(project); } public async updateProjectForRoundTrip(project: Project) { @@ -563,13 +562,13 @@ export class ProjectsController { } if (!project.importedTargets.includes(constants.NetCoreTargets)) { - const result = await this.apiWrapper.showWarningMessage(constants.updateProjectForRoundTrip, constants.yesString, constants.noString); + const result = await vscode.window.showWarningMessage(constants.updateProjectForRoundTrip, constants.yesString, constants.noString); if (result === constants.yesString) { await project.updateProjectForRoundTrip(); await project.updateSystemDatabaseReferencesInProjFile(); } } else if (project.containsSSDTOnlySystemDatabaseReferences()) { - const result = await this.apiWrapper.showWarningMessage(constants.updateProjectDatabaseReferencesForRoundTrip, constants.yesString, constants.noString); + const result = await vscode.window.showWarningMessage(constants.updateProjectDatabaseReferencesForRoundTrip, constants.yesString, constants.noString); if (result === constants.yesString) { await project.updateSystemDatabaseReferencesInProjFile(); } @@ -590,7 +589,7 @@ export class ProjectsController { } public async getDaxFxService(): Promise { - const ext: Extension = extensions.getExtension(mssql.extension.name)!; + const ext: vscode.Extension = vscode.extensions.getExtension(mssql.extension.name)!; await ext.activate(); return (ext.exports as mssql.IExtension).dacFx; @@ -616,7 +615,7 @@ export class ProjectsController { // TODO: ask project for suggested name that doesn't conflict const suggestedName = itemType.friendlyName.replace(new RegExp('\s', 'g'), '') + '1'; - const itemObjectName = await this.apiWrapper.showInputBox({ + const itemObjectName = await vscode.window.showInputBox({ prompt: constants.newObjectNamePrompt(itemType.friendlyName), value: suggestedName, }); @@ -632,7 +631,7 @@ export class ProjectsController { * Imports a new SQL database project from the existing database, * prompting the user for a name, file path location and extract target */ - public async importNewDatabaseProject(context: IConnectionProfile | any): Promise { + public async importNewDatabaseProject(context: azdata.IConnectionProfile | any): Promise { // TODO: Refactor code try { @@ -641,14 +640,12 @@ export class ProjectsController { if (!model) { return; // cancelled by user } - model.projName = await this.getProjectName(model.database); let newProjFolderUri = (await this.getFolderLocation()).fsPath; model.extractTarget = await this.getExtractTarget(); model.version = '1.0.0.0'; - const newProjFilePath = await this.createNewProject(model.projName, Uri.file(newProjFolderUri), true); - + const newProjFilePath = await this.createNewProject(model.projName, vscode.Uri.file(newProjFolderUri), true); model.filePath = path.dirname(newProjFilePath); if (model.extractTarget === mssql.ExtractTarget.file) { @@ -656,15 +653,14 @@ export class ProjectsController { } const project = await Project.openProject(newProjFilePath); - await this.importApiCall(model); // Call ExtractAPI in DacFx Service let fileFolderList: string[] = model.extractTarget === mssql.ExtractTarget.file ? [model.filePath] : await this.generateList(model.filePath); // Create a list of all the files and directories to be added to project await project.addToProject(fileFolderList); // Add generated file structure to the project - await this.openProject(Uri.file(newProjFilePath)); + await this.openProject(vscode.Uri.file(newProjFilePath)); } catch (err) { - this.apiWrapper.showErrorMessage(utils.getErrorMessage(err)); + vscode.window.showErrorMessage(utils.getErrorMessage(err)); } } @@ -680,7 +676,7 @@ export class ProjectsController { connectionId = profile.id; } else { - const connection = await this.apiWrapper.openConnectionDialog(); + const connection = await azdata.connection.openConnectionDialog(); if (!connection) { return undefined; @@ -695,8 +691,8 @@ export class ProjectsController { // choose database if connection was to a server or master if (!model.database || model.database === constants.master) { - const databaseList = await this.apiWrapper.listDatabases(connectionId); - database = (await this.apiWrapper.showQuickPick(databaseList.map(dbName => { return { label: dbName }; }), + const databaseList = await azdata.connection.listDatabases(connectionId); + database = (await vscode.window.showQuickPick(databaseList.map(dbName => { return { label: dbName }; }), { canPickMany: false, placeHolder: constants.extractDatabaseSelection @@ -714,7 +710,7 @@ export class ProjectsController { return model; } - private getConnectionProfileFromContext(context: IConnectionProfile | any): IConnectionProfile | undefined { + private getConnectionProfileFromContext(context: azdata.IConnectionProfile | any): azdata.IConnectionProfile | undefined { if (!context) { return undefined; } @@ -725,7 +721,7 @@ export class ProjectsController { } private async getProjectName(dbName: string): Promise { - let projName = await this.apiWrapper.showInputBox({ + let projName = await vscode.window.showInputBox({ prompt: constants.newDatabaseProjectName, value: `DatabaseProject${dbName}` }); @@ -757,7 +753,7 @@ export class ProjectsController { private async getExtractTarget(): Promise { let extractTarget: mssql.ExtractTarget; - let extractTargetOptions: QuickPickItem[] = []; + let extractTargetOptions: vscode.QuickPickItem[] = []; let keys = [constants.file, constants.flat, constants.objectType, constants.schema, constants.schemaObjectType]; @@ -766,7 +762,7 @@ export class ProjectsController { extractTargetOptions.push({ label: targetOption }); }); - let input = await this.apiWrapper.showQuickPick(extractTargetOptions, { + let input = await vscode.window.showQuickPick(extractTargetOptions, { canPickMany: false, placeHolder: constants.extractTargetInput }); @@ -777,19 +773,19 @@ export class ProjectsController { return extractTarget; } - private async getFolderLocation(): Promise { - let projUri: Uri; + private async getFolderLocation(): Promise { + let projUri: vscode.Uri; - const selectionResult = await this.apiWrapper.showOpenDialog({ + const selectionResult = await vscode.window.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, openLabel: constants.selectString, - defaultUri: this.apiWrapper.workspaceFolders() ? (this.apiWrapper.workspaceFolders() as WorkspaceFolder[])[0].uri : undefined + defaultUri: vscode.workspace.workspaceFolders ? (vscode.workspace.workspaceFolders as vscode.WorkspaceFolder[])[0].uri : undefined }); if (selectionResult) { - projUri = (selectionResult as Uri[])[0]; + projUri = (selectionResult as vscode.Uri[])[0]; } else { throw new Error(constants.projectLocationRequired); @@ -799,12 +795,12 @@ export class ProjectsController { } public async importApiCall(model: ImportDataModel): Promise { - let ext = this.apiWrapper.getExtension(mssql.extension.name)!; + let ext = vscode.extensions.getExtension(mssql.extension.name)!; const service = (await ext.activate() as mssql.IExtension).dacFx; - const ownerUri = await this.apiWrapper.getUriForConnection(model.serverId); + const ownerUri = await azdata.connection.getUriForConnection(model.serverId); - await service.importDatabaseProject(model.database, model.filePath, model.projName, model.version, ownerUri, model.extractTarget, TaskExecutionMode.execute); + await service.importDatabaseProject(model.database, model.filePath, model.projName, model.version, ownerUri, model.extractTarget, azdata.TaskExecutionMode.execute); // TODO: Check for success; throw error } @@ -819,7 +815,7 @@ export class ProjectsController { if (await utils.exists(absolutePath + constants.sqlFileExtension)) { absolutePath += constants.sqlFileExtension; } else { - await this.apiWrapper.showErrorMessage(constants.cannotResolvePath(absolutePath)); + vscode.window.showErrorMessage(constants.cannotResolvePath(absolutePath)); return fileFolderList; } } diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts index 0eebe0f627..f7ed66dada 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts @@ -10,7 +10,6 @@ import * as utils from '../common/utils'; import { Project } from '../models/project'; import { SqlConnectionDataSource } from '../models/dataSources/sqlConnectionStringSource'; -import { ApiWrapper } from '../common/apiWrapper'; import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSettings'; interface DataSourceDropdownValue extends azdata.CategoryValue { @@ -42,7 +41,7 @@ export class PublishDatabaseDialog { public generateScript: ((proj: Project, profile: IGenerateScriptSettings) => any) | undefined; public readPublishProfile: ((profileUri: vscode.Uri) => any) | undefined; - constructor(private apiWrapper: ApiWrapper, private project: Project) { + constructor(private project: Project) { this.dialog = azdata.window.createModelViewDialog(constants.publishDialogName); this.publishTab = azdata.window.createTab(constants.publishDialogName); } @@ -172,10 +171,10 @@ export class PublishDatabaseDialog { }; if (dataSource.integratedSecurity) { - connId = (await this.apiWrapper.connectionConnect(connProfile, false, false)).connectionId; + connId = (await azdata.connection.connect(connProfile, false, false)).connectionId; } else { - connId = (await this.apiWrapper.openConnectionDialog(undefined, connProfile)).connectionId; + connId = (await azdata.connection.openConnectionDialog(undefined, connProfile)).connectionId; } } else { @@ -186,7 +185,7 @@ export class PublishDatabaseDialog { connId = this.connection?.connectionId; } - return await this.apiWrapper.getUriForConnection(connId); + return await azdata.connection.getUriForConnection(connId); } catch (err) { throw new Error(constants.unableToCreatePublishConnection + ': ' + utils.getErrorMessage(err)); @@ -202,7 +201,7 @@ export class PublishDatabaseDialog { sqlCmdVariables: sqlCmdVars }; - this.apiWrapper.closeDialog(this.dialog); + azdata.window.closeDialog(this.dialog); await this.publish!(this.project, settings); this.dispose(); @@ -216,7 +215,7 @@ export class PublishDatabaseDialog { sqlCmdVariables: sqlCmdVars }; - this.apiWrapper.closeDialog(this.dialog); + azdata.window.closeDialog(this.dialog); if (this.generateScript) { await this.generateScript!(this.project, settings); @@ -360,7 +359,7 @@ export class PublishDatabaseDialog { }).component(); editConnectionButton.onDidClick(async () => { - this.connection = await this.apiWrapper.openConnectionDialog(); + this.connection = await azdata.connection.openConnectionDialog(); // show connection name if there is one, otherwise show connection string if (this.connection.options['connectionName']) { @@ -401,7 +400,7 @@ export class PublishDatabaseDialog { }).component(); loadProfileButton.onDidClick(async () => { - const fileUris = await this.apiWrapper.showOpenDialog( + const fileUris = await vscode.window.showOpenDialog( { canSelectFiles: true, canSelectFolders: false, diff --git a/extensions/sql-database-projects/src/extension.ts b/extensions/sql-database-projects/src/extension.ts index 2d55d3fa5e..dfd92eb254 100644 --- a/extensions/sql-database-projects/src/extension.ts +++ b/extensions/sql-database-projects/src/extension.ts @@ -5,13 +5,12 @@ import * as vscode from 'vscode'; import MainController from './controllers/mainController'; -import { ApiWrapper } from './common/apiWrapper'; let controllers: MainController[] = []; export async function activate(context: vscode.ExtensionContext): Promise { // Start the main controller - const mainController = new MainController(context, new ApiWrapper()); + const mainController = new MainController(context); controllers.push(mainController); context.subscriptions.push(mainController); diff --git a/extensions/sql-database-projects/src/test/mainController.test.ts b/extensions/sql-database-projects/src/test/mainController.test.ts index 7ea17c2179..9e9bef3868 100644 --- a/extensions/sql-database-projects/src/test/mainController.test.ts +++ b/extensions/sql-database-projects/src/test/mainController.test.ts @@ -6,15 +6,14 @@ import * as should from 'should'; import * as path from 'path'; import * as os from 'os'; -import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; +import * as sinon from 'sinon'; import * as baselines from './baselines/baselines'; import * as templates from '../templates/templates'; import * as constants from '../common/constants'; - import { createContext, TestContext } from './testContext'; import MainController from '../controllers/mainController'; -import { shouldThrowSpecificError, generateTestFolderPath, createTestProject } from './testUtils'; +import { generateTestFolderPath, createTestProject } from './testUtils'; let testContext: TestContext; @@ -25,22 +24,18 @@ describe('MainController: main controller operations', function (): void { await baselines.loadBaselines(); }); - beforeEach(function (): void { - testContext.apiWrapper.reset(); + afterEach(function (): void { + sinon.restore(); }); it('Should create new project through MainController', async function (): Promise { const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`); - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('MyProjectName')); - testContext.apiWrapper.setup(x => x.showOpenDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve([vscode.Uri.file(projFileDir)])); - testContext.apiWrapper.setup(x => x.workspaceFolders()).returns(() => undefined); - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { - console.log(s); - return Promise.resolve(s); - }); + sinon.stub(vscode.window, 'showInputBox').resolves('MyProjectName'); + sinon.stub(vscode.window, 'showOpenDialog').resolves([vscode.Uri.file(projFileDir)]); + sinon.replaceGetter(vscode.workspace, 'workspaceFolders', () => undefined); - const controller = new MainController(testContext.context, testContext.apiWrapper.object); + const controller = new MainController(testContext.context); const proj = await controller.createNewProject(); should(proj).not.equal(undefined); @@ -48,30 +43,33 @@ describe('MainController: main controller operations', function (): void { it('Should show error when no project name', async function (): Promise { for (const name of ['', ' ', undefined]) { - testContext.apiWrapper.reset(); - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(name)); - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - const controller = new MainController(testContext.context, testContext.apiWrapper.object); - await shouldThrowSpecificError(async () => await controller.createNewProject(), constants.projectNameRequired, `case: '${name}'`); + const stub = sinon.stub(vscode.window, 'showInputBox').resolves(name); + const spy = sinon.spy(vscode.window, 'showErrorMessage'); + const controller = new MainController(testContext.context); + await controller.createNewProject(); + should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once'); + should(spy.calledWith(constants.projectNameRequired)).be.true(`showErrorMessage not called with expected message '${constants.projectNameRequired}' Actual '${spy.getCall(0).args[0]}'`); + stub.restore(); + spy.restore(); } }); it('Should show error when no location name', async function (): Promise { - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('MyProjectName')); - testContext.apiWrapper.setup(x => x.showOpenDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - const controller = new MainController(testContext.context, testContext.apiWrapper.object); - await shouldThrowSpecificError(async () => await controller.createNewProject(), constants.projectLocationRequired); + sinon.stub(vscode.window, 'showInputBox').resolves('MyProjectName'); + sinon.stub(vscode.window, 'showOpenDialog').resolves(undefined); + const spy = sinon.spy(vscode.window, 'showErrorMessage'); + const controller = new MainController(testContext.context); + await controller.createNewProject(); + should(spy.calledOnce).be.true('showErrorMessage should be called exactly once'); + should(spy.calledWith(constants.projectLocationRequired)).be.true(`showErrorMessage not called with expected message '${constants.projectLocationRequired}' Actual '${spy.getCall(0).args[0]}'`); }); it('Should create new instance without error', async function (): Promise { - should.doesNotThrow(() => new MainController(testContext.context, testContext.apiWrapper.object), 'Creating controller should not throw an error'); + should.doesNotThrow(() => new MainController(testContext.context), 'Creating controller should not throw an error'); }); it('Should activate and deactivate without error', async function (): Promise { - let controller = new MainController(testContext.context, testContext.apiWrapper.object); + let controller = new MainController(testContext.context); should.notEqual(controller.extensionContext, undefined); should.doesNotThrow(() => controller.activate(), 'activate() should not throw an error'); @@ -84,14 +82,14 @@ describe('MainController: main controller operations', function (): void { const nestedFolder = path.join(rootFolderPath, 'nestedProject'); const nestedProject = await createTestProject(baselines.openProjectFileBaseline, nestedFolder); - testContext.apiWrapper.setup(x => x.workspaceFolders()).returns(() => [workspaceFolder]); const workspaceFolder: vscode.WorkspaceFolder = { uri: vscode.Uri.file(rootFolderPath), name: '', index: 0 }; + sinon.replaceGetter(vscode.workspace, 'workspaceFolders', () => [workspaceFolder]); - const controller = new MainController(testContext.context, testContext.apiWrapper.object); + const controller = new MainController(testContext.context); should(controller.projController.projects.length).equal(0); await controller.loadProjectsInWorkspace(); diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index 923c8bcb14..270682a7a6 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -9,6 +9,7 @@ import * as os from 'os'; import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; import * as baselines from './baselines/baselines'; import * as templates from '../templates/templates'; import * as testUtils from './testUtils'; @@ -20,7 +21,6 @@ import { promises as fs } from 'fs'; import { createContext, TestContext, mockDacFxResult } from './testContext'; import { Project, SystemDatabase, ProjectEntry, reservedProjectFolders } from '../models/project'; import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog'; -import { ApiWrapper } from '../common/apiWrapper'; import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSettings'; import { exists } from '../common/utils'; import { ProjectRootTreeItem } from '../models/tree/projectTreeItem'; @@ -46,509 +46,537 @@ const mockConnectionProfile: azdata.IConnectionProfile = { options: undefined as any }; -beforeEach(function (): void { - testContext = createContext(); -}); -describe('ProjectsController: project controller operations', function (): void { - before(async function (): Promise { - await templates.loadTemplates(path.join(__dirname, '..', '..', 'resources', 'templates')); - await baselines.loadBaselines(); + +describe ('ProjectsController', function(): void { + beforeEach(function (): void { + testContext = createContext(); }); - describe('Project file operations and prompting', function (): void { - it('Should create new sqlproj file with correct values', async function (): Promise { - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`); + afterEach(function (): void { + sinon.restore(); + }); - const projFilePath = await projController.createNewProject('TestProjectName', vscode.Uri.file(projFileDir), false, 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575'); - - let projFileText = (await fs.readFile(projFilePath)).toString(); - - should(projFileText).equal(baselines.newProjectFileBaseline); + describe('project controller operations', function (): void { + before(async function (): Promise { + await templates.loadTemplates(path.join(__dirname, '..', '..', 'resources', 'templates')); + await baselines.loadBaselines(); }); - it('Should load Project and associated DataSources', async function (): Promise { + describe('Project file operations and prompting', function (): void { + it('Should create new sqlproj file with correct values', async function (): Promise { + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`); + + const projFilePath = await projController.createNewProject('TestProjectName', vscode.Uri.file(projFileDir), false, 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575'); + + let projFileText = (await fs.readFile(projFilePath)).toString(); + + should(projFileText).equal(baselines.newProjectFileBaseline); + }); + + it('Should load Project and associated DataSources', async function (): Promise { + // setup test files + const folderPath = await testUtils.generateTestFolderPath(); + const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline, folderPath); + await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + + const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); + + should(project.files.length).equal(8); // detailed sqlproj tests in their own test file + should(project.dataSources.length).equal(2); // detailed datasources tests in their own test file + }); + + it('Should not keep failed to load project in project list.', async function (): Promise { + const folderPath = await testUtils.generateTestFolderPath(); + const sqlProjPath = await testUtils.createTestSqlProjFile('empty file with no valid xml', folderPath); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + + try { + await projController.openProject(vscode.Uri.file(sqlProjPath)); + should.fail(null, null, 'The given project not expected to open'); + } + catch { + should(projController.projects.length).equal(0, 'The added project should be removed'); + } + }); + + it('Should return silently when no SQL object name provided in prompts', async function (): Promise { + for (const name of ['', ' ', undefined]) { + const showInputBoxStub = sinon.stub(vscode.window, 'showInputBox').resolves(name); + const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + const project = new Project('FakePath'); + + should(project.files.length).equal(0); + await projController.addItemPrompt(new Project('FakePath'), '', templates.script); + should(project.files.length).equal(0, 'Expected to return without throwing an exception or adding a file when an empty/undefined name is provided.'); + should(showErrorMessageSpy.notCalled).be.true('showErrorMessage should not have been called'); + showInputBoxStub.restore(); + showErrorMessageSpy.restore(); + } + }); + + it('Should show error if trying to add a file that already exists', async function (): Promise { + const tableName = 'table1'; + sinon.stub(vscode.window, 'showInputBox').resolves(tableName); + const spy = sinon.spy(vscode.window, 'showErrorMessage'); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + const project = await testUtils.createTestProject(baselines.newProjectFileBaseline); + + should(project.files.length).equal(0, 'There should be no files'); + await projController.addItemPrompt(project, '', templates.script); + should(project.files.length).equal(1, 'File should be successfully added'); + await projController.addItemPrompt(project, '', templates.script); + const msg = constants.fileAlreadyExists(tableName); + 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 trying to add a folder that already exists', async function (): Promise { + const folderName = 'folder1'; + const stub = sinon.stub(vscode.window, 'showInputBox').resolves(folderName); + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + const project = await testUtils.createTestProject(baselines.newProjectFileBaseline); + const projectRoot = new ProjectRootTreeItem(project); + + should(project.files.length).equal(0, 'There should be no other folders'); + await projController.addFolderPrompt(projectRoot); + should(project.files.length).equal(1, 'Folder should be successfully added'); + projController.refreshProjectsTree(); + stub.restore(); + await verifyFolderNotAdded(folderName, projController, project, projectRoot); + + // reserved folder names + for (let i in reservedProjectFolders) { + await verifyFolderNotAdded(reservedProjectFolders[i], projController, project, projectRoot); + } + }); + + it('Should be able to add folder with reserved name as long as not at project root', async function (): Promise { + const folderName = 'folder1'; + const stub = sinon.stub(vscode.window, 'showInputBox').resolves(folderName); + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + const project = await testUtils.createTestProject(baselines.openProjectFileBaseline); + const projectRoot = new ProjectRootTreeItem(project); + + // make sure it's ok to add these folders if they aren't where the reserved folders are at the root of the project + let node = projectRoot.children.find(c => c.friendlyName === 'Tables'); + stub.restore(); + for (let i in reservedProjectFolders) { + await verifyFolderAdded(reservedProjectFolders[i], projController, project, node); + } + }); + + async function verifyFolderAdded(folderName: string, projController: ProjectsController, project: Project, node: BaseProjectTreeItem): Promise { + const beforeFileCount = project.files.length; + const stub = sinon.stub(vscode.window, 'showInputBox').resolves(folderName); + await projController.addFolderPrompt(node); + should(project.files.length).equal(beforeFileCount + 1, `File count should be increased by one after adding the folder ${folderName}`); + stub.restore(); + } + + async function verifyFolderNotAdded(folderName: string, projController: ProjectsController, project: Project, node: BaseProjectTreeItem): Promise { + const beforeFileCount = project.files.length; + const showInputBoxStub = sinon.stub(vscode.window, 'showInputBox').resolves(folderName); + const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); + await projController.addFolderPrompt(node); + should(showErrorMessageSpy.calledOnce).be.true('showErrorMessage should have been called exactly once'); + const msg = constants.folderAlreadyExists(folderName); + should(showErrorMessageSpy.calledWith(msg)).be.true(`showErrorMessage not called with expected message '${msg}' Actual '${showErrorMessageSpy.getCall(0).args[0]}'`); + should(project.files.length).equal(beforeFileCount, 'File count should be the same as before the folder was attempted to be added'); + showInputBoxStub.restore(); + showErrorMessageSpy.restore(); + } + + it('Should delete nested ProjectEntry from node', async function (): Promise { + let proj = await testUtils.createTestProject(templates.newSqlProjectTemplate); + const setupResult = await setupDeleteExcludeTest(proj); + const scriptEntry = setupResult[0], projTreeRoot = setupResult[1]; + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + + await projController.delete(projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!.children[0] /* LowerFolder */); + + proj = await Project.openProject(proj.projectFilePath); // reload edited sqlproj from disk + + // confirm result + should(proj.files.length).equal(1, 'number of file/folder entries'); // lowerEntry and the contained scripts should be deleted + should(proj.files[0].relativePath).equal('UpperFolder'); + + should(await exists(scriptEntry.fsUri.fsPath)).equal(false, 'script is supposed to be deleted'); + }); + + it('Should exclude nested ProjectEntry from node', async function (): Promise { + let proj = await testUtils.createTestProject(templates.newSqlProjectTemplate); + const setupResult = await setupDeleteExcludeTest(proj); + const scriptEntry = setupResult[0], projTreeRoot = setupResult[1]; + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + + await projController.exclude(projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!.children[0] /* LowerFolder */); + + proj = await Project.openProject(proj.projectFilePath); // reload edited sqlproj from disk + + // confirm result + should(proj.files.length).equal(1, 'number of file/folder entries'); // LowerFolder and the contained scripts should be deleted + should(proj.files[0].relativePath).equal('UpperFolder'); // UpperFolder should still be there + + should(await exists(scriptEntry.fsUri.fsPath)).equal(true, 'script is supposed to still exist on disk'); + }); + }); + + describe('Publishing and script generation', function (): void { + it('Publish dialog should open from ProjectController', async function (): Promise { + let opened = false; + + let publishDialog = TypeMoq.Mock.ofType(PublishDatabaseDialog); + publishDialog.setup(x => x.openDialog()).returns(() => { opened = true; }); + + let projController = TypeMoq.Mock.ofType(ProjectsController); + projController.callBase = true; + projController.setup(x => x.getPublishDialog(TypeMoq.It.isAny())).returns(() => publishDialog.object); + + await projController.object.publishProject(new Project('FakePath')); + should(opened).equal(true); + }); + + it('Callbacks are hooked up and called from Publish dialog', async function (): Promise { + const projPath = path.dirname(await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline)); + await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, projPath); + const proj = new Project(projPath); + + const publishHoller = 'hello from callback for publish()'; + const generateHoller = 'hello from callback for generateScript()'; + const profileHoller = 'hello from callback for readPublishProfile()'; + + let holler = 'nothing'; + + let publishDialog = TypeMoq.Mock.ofType(PublishDatabaseDialog, undefined, undefined, proj); + publishDialog.callBase = true; + publishDialog.setup(x => x.getConnectionUri()).returns(() => Promise.resolve('fake|connection|uri')); + + let projController = TypeMoq.Mock.ofType(ProjectsController); + projController.callBase = true; + projController.setup(x => x.getPublishDialog(TypeMoq.It.isAny())).returns(() => publishDialog.object); + projController.setup(x => x.executionCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IPublishSettings => true))).returns(() => { + holler = publishHoller; + return Promise.resolve(undefined); + }); + projController.setup(x => x.readPublishProfile(TypeMoq.It.isAny())).returns(() => { + holler = profileHoller; + return Promise.resolve({ + databaseName: '', + sqlCmdVariables: {} + }); + }); + + projController.setup(x => x.executionCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IGenerateScriptSettings => true))).returns(() => { + holler = generateHoller; + return Promise.resolve(undefined); + }); + + let dialog = await projController.object.publishProject(proj); + await dialog.publishClick(); + + should(holler).equal(publishHoller, 'executionCallback() is supposed to have been setup and called for Publish scenario'); + + dialog = await projController.object.publishProject(proj); + await dialog.generateScriptClick(); + + should(holler).equal(generateHoller, 'executionCallback() is supposed to have been setup and called for GenerateScript scenario'); + + dialog = await projController.object.publishProject(proj); + await projController.object.readPublishProfile(vscode.Uri.parse('test')); + + should(holler).equal(profileHoller, 'executionCallback() is supposed to have been setup and called for ReadPublishProfile scenario'); + }); + + it('Should read database name and SQLCMD variables from publish profile', async function (): Promise { + await baselines.loadBaselines(); + let profilePath = await testUtils.createTestFile(baselines.publishProfileBaseline, 'publishProfile.publish.xml'); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + + let result = await projController.readPublishProfile(vscode.Uri.file(profilePath)); + should(result.databaseName).equal('targetDb'); + should(Object.keys(result.sqlCmdVariables).length).equal(1); + should(result.sqlCmdVariables['ProdDatabaseName']).equal('MyProdDatabase'); + }); + + it('Should copy dacpac to temp folder before publishing', async function (): Promise { + const fakeDacpacContents = 'SwiftFlewHiawathasArrow'; + let postCopyContents = ''; + let builtDacpacPath = ''; + let publishedDacpacPath = ''; + + testContext.dacFxService.setup(x => x.generateDeployScript(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async (p) => { + publishedDacpacPath = p; + postCopyContents = (await fs.readFile(publishedDacpacPath)).toString(); + return Promise.resolve(mockDacFxResult); + }); + + let projController = TypeMoq.Mock.ofType(ProjectsController); + projController.callBase = true; + + projController.setup(x => x.buildProject(TypeMoq.It.isAny())).returns(async () => { + builtDacpacPath = await testUtils.createTestFile(fakeDacpacContents, 'output.dacpac'); + return builtDacpacPath; + }); + + projController.setup(x => x.getDaxFxService()).returns(() => Promise.resolve(testContext.dacFxService.object)); + + await projController.object.executionCallback(new Project(''), { connectionUri: '', databaseName: '' }); + + should(builtDacpacPath).not.equal('', 'built dacpac path should be set'); + should(publishedDacpacPath).not.equal('', 'published dacpac path should be set'); + should(builtDacpacPath).not.equal(publishedDacpacPath, 'built and published dacpac paths should be different'); + should(postCopyContents).equal(fakeDacpacContents, 'contents of built and published dacpacs should match'); + }); + }); + }); + + describe('import operations', function (): void { + it('Should create list of all files and folders correctly', async function (): Promise { + const testFolderPath = await testUtils.createDummyFileStructure(); + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + const fileList = await projController.generateList(testFolderPath); + + should(fileList.length).equal(15); // Parent folder + 2 files under parent folder + 2 directories with 5 files each + }); + + it('Should error out for inaccessible path', async function (): Promise { + const spy = sinon.spy(vscode.window, 'showErrorMessage'); + + let testFolderPath = await testUtils.generateTestFolderPath(); + testFolderPath += '_nonexistentFolder'; // Modify folder path to point to a nonexistent location + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + + await projController.generateList(testFolderPath); + should(spy.calledOnce).be.true('showErrorMessage should have been called'); + const msg = constants.cannotResolvePath(testFolderPath); + should(spy.calledWith(msg)).be.true(`showErrorMessage not called with expected message '${msg}' Actual '${spy.getCall(0).args[0]}'`); + }); + + it('Should show error when no project name provided', async function (): Promise { + for (const name of ['', ' ', undefined]) { + sinon.stub(vscode.window, 'showInputBox').resolves(name); + const spy = sinon.spy(vscode.window, 'showErrorMessage'); + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + await projController.importNewDatabaseProject({ connectionProfile: mockConnectionProfile }); + should(spy.calledOnce).be.true('showErrorMessage should have been called'); + should(spy.calledWith(constants.projectNameRequired)).be.true(`showErrorMessage not called with expected message '${constants.projectNameRequired}' Actual '${spy.getCall(0).args[0]}'`); + sinon.restore(); + } + }); + + it('Should show error when no target information provided', async function (): Promise { + sinon.stub(vscode.window, 'showInputBox').resolves('MyProjectName'); + sinon.stub(vscode.window, 'showQuickPick').resolves(undefined); + sinon.stub(vscode.window, 'showOpenDialog').resolves([vscode.Uri.file('fakePath')]); + const spy = sinon.spy(vscode.window, 'showErrorMessage'); + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + await projController.importNewDatabaseProject({ connectionProfile: mockConnectionProfile }); + should(spy.calledOnce).be.true('showErrorMessage should have been called'); + should(spy.calledWith(constants.extractTargetRequired)).be.true(`showErrorMessage not called with expected message '${constants.extractTargetRequired}' Actual '${spy.getCall(0).args[0]}'`); + }); + + it('Should show error when no location provided with ExtractTarget = File', async function (): Promise { + sinon.stub(vscode.window, 'showInputBox').resolves('MyProjectName'); + sinon.stub(vscode.window, 'showOpenDialog').resolves(undefined); + sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.file }); + const spy = sinon.spy(vscode.window, 'showErrorMessage'); + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + await projController.importNewDatabaseProject({ connectionProfile: mockConnectionProfile }); + should(spy.calledOnce).be.true('showErrorMessage should have been called'); + should(spy.calledWith(constants.projectLocationRequired)).be.true(`showErrorMessage not called with expected message '${constants.projectLocationRequired}' Actual '${spy.getCall(0).args[0]}'`); + }); + + it('Should show error when no location provided with ExtractTarget = SchemaObjectType', async function (): Promise { + sinon.stub(vscode.window, 'showInputBox').resolves('MyProjectName'); + sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.schemaObjectType }); + sinon.stub(vscode.window, 'showOpenDialog').resolves(undefined); + const spy = sinon.spy(vscode.window, 'showErrorMessage'); + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + await projController.importNewDatabaseProject({ connectionProfile: mockConnectionProfile }); + should(spy.calledOnce).be.true('showErrorMessage should have been called'); + should(spy.calledWith(constants.projectLocationRequired)).be.true(`showErrorMessage not called with expected message '${constants.projectLocationRequired}' Actual '${spy.getCall(0).args[0]}'`); + }); + + it('Should set model filePath correctly for ExtractType = File and not-File.', async function (): Promise { + const projectName = 'MyProjectName'; + let folderPath = await testUtils.generateTestFolderPath(); + + sinon.stub(vscode.window, 'showInputBox').resolves(projectName); + const showQuickPickStub = sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.file }); + sinon.stub(vscode.window, 'showOpenDialog').callsFake(() => Promise.resolve([vscode.Uri.file(folderPath)])); + + let importPath; + + let projController = TypeMoq.Mock.ofType(ProjectsController, undefined, undefined, new SqlDatabaseProjectTreeViewProvider()); + projController.callBase = true; + + projController.setup(x => x.importApiCall(TypeMoq.It.isAny())).returns(async (model) => { console.log('CALLED'); importPath = model.filePath; }); + + await projController.object.importNewDatabaseProject({ connectionProfile: mockConnectionProfile }); + should(importPath).equal(vscode.Uri.file(path.join(folderPath, projectName, projectName + '.sql')).fsPath, `model.filePath should be set to a specific file for ExtractTarget === file, but was ${importPath}`); + + // reset for counter-test + importPath = undefined; + folderPath = await testUtils.generateTestFolderPath(); + showQuickPickStub.resolves({ label: constants.schemaObjectType }); + + await projController.object.importNewDatabaseProject({ connectionProfile: mockConnectionProfile }); + should(importPath).equal(vscode.Uri.file(path.join(folderPath, projectName)).fsPath, `model.filePath should be set to a folder for ExtractTarget !== file, but was ${importPath}`); + }); + + it('Should establish Import context correctly for ObjectExplorer and palette launch points', async function (): Promise { + const connectionId = 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575'; + // test welcome button and palette launch points (context-less) + let mockDbSelection = 'FakeDatabase'; + sinon.stub(azdata.connection, 'listDatabases').resolves([]); + sinon.stub(vscode.window, 'showQuickPick').resolves({ label: mockDbSelection }); + sinon.stub(azdata.connection, 'openConnectionDialog').resolves({ + providerName: 'MSSQL', + connectionId: connectionId, + options: {} + }); + + let projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + + let result = await projController.getModelFromContext(undefined); + + should(result).deepEqual({database: mockDbSelection, serverId: connectionId}); + + // test launch via Object Explorer context + result = await projController.getModelFromContext(mockConnectionProfile); + should(result).deepEqual({database: 'My Database', serverId: 'My Id'}); + }); + }); + + describe('add database reference operations', function (): void { + it('Should show error when no reference type is selected', async function (): Promise { + sinon.stub(vscode.window, 'showQuickPick').resolves(undefined); + const spy = sinon.spy(vscode.window, 'showErrorMessage'); + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + await projController.addDatabaseReference(new Project('FakePath')); + should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once'); + should(spy.calledWith(constants.databaseReferenceTypeRequired)).be.true(`showErrorMessage not called with expected message '${constants.databaseReferenceTypeRequired}' Actual '${spy.getCall(0).args[0]}'`); + }); + + it('Should show error when no file is selected', async function (): Promise { + sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.dacpac }); + sinon.stub(vscode.window, 'showOpenDialog').resolves(undefined); + const spy = sinon.spy(vscode.window, 'showErrorMessage'); + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + await projController.addDatabaseReference(new Project('FakePath')); + should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once'); + should(spy.calledWith(constants.dacpacFileLocationRequired)).be.true(`showErrorMessage not called with expected message '${constants.dacpacFileLocationRequired}' Actual '${spy.getCall(0).args[0]}'`); + }); + + it('Should show error when no database name is provided', async function (): Promise { + sinon.stub(vscode.window, 'showInputBox').resolves(undefined); + sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.dacpac }); + sinon.stub(vscode.window, 'showOpenDialog').resolves([vscode.Uri.file('FakePath')]); + const spy = sinon.spy(vscode.window, 'showErrorMessage'); + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + await projController.addDatabaseReference(new Project('FakePath')); + should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once'); + should(spy.calledWith(constants.databaseNameRequired)).be.true(`showErrorMessage not called with expected message '${constants.databaseNameRequired}' Actual '${spy.getCall(0).args[0]}'`); + }); + + it('Should return the correct system database', async function (): Promise { + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + const projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); + const project = await Project.openProject(projFilePath); + + const stub = sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.master }); + let systemDb = await projController.getSystemDatabaseName(project); + should.equal(systemDb, SystemDatabase.master); + + stub.resolves({ label: constants.msdb }); + systemDb = await projController.getSystemDatabaseName(project); + should.equal(systemDb, SystemDatabase.msdb); + + stub.resolves(undefined); + await testUtils.shouldThrowSpecificError(async () => await projController.getSystemDatabaseName(project), constants.systemDatabaseReferenceRequired); + }); + }); + + describe.skip('ProjectsController: round trip feature with SSDT', function (): void { + it('Should show warning message for SSDT project opened in Azure Data Studio', async function (): Promise { + const stub = sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.noString)); + + // setup test files + const folderPath = await testUtils.generateTestFolderPath(); + const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); + await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + + await projController.openProject(vscode.Uri.file(sqlProjPath)); + should(stub.calledOnce).be.true('showWarningMessage should have been called exactly once'); + should(stub.calledWith(constants.updateProjectForRoundTrip)).be.true(`showWarningMessage not called with expected message '${constants.updateProjectForRoundTrip}' Actual '${stub.getCall(0).args[0]}'`); + }); + + it('Should not show warning message for non-SSDT projects that have the additional information for Build', async function (): Promise { // setup test files const folderPath = await testUtils.generateTestFolderPath(); const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline, folderPath); await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + + const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); // no error thrown + + should(project.importedTargets.length).equal(3); // additional target should exist by default + }); + + it('Should not update project and no backup file should be created when update to project is rejected', async function (): Promise { + sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.noString)); + // setup test files + const folderPath = await testUtils.generateTestFolderPath(); + const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); + await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); - should(project.files.length).equal(8); // detailed sqlproj tests in their own test file - should(project.dataSources.length).equal(2); // detailed datasources tests in their own test file + should(await exists(sqlProjPath + '_backup')).equal(false); // backup file should not be generated + should(project.importedTargets.length).equal(2); // additional target should not be added by updateProjectForRoundTrip method }); - it('Should not keep failed to load project in project list.', async function (): Promise { + it('Should load Project and associated import targets when update to project is accepted', async function (): Promise { + sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.yesString)); + + // setup test files const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile('empty file with no valid xml', folderPath); - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); + const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); + await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); - try { - await projController.openProject(vscode.Uri.file(sqlProjPath)); - should.fail(null, null, 'The given project not expected to open'); - } - catch { - should(projController.projects.length).equal(0, 'The added project should be removed'); - } - }); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); - it('Should return silently when no SQL object name provided in prompts', async function (): Promise { - for (const name of ['', ' ', undefined]) { - testContext.apiWrapper.reset(); - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(name)); - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); + const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - const project = new Project('FakePath'); - - should(project.files.length).equal(0); - await projController.addItemPrompt(new Project('FakePath'), '', templates.script); - should(project.files.length).equal(0, 'Expected to return without throwing an exception or adding a file when an empty/undefined name is provided.'); - } - }); - - it('Should show error if trying to add a file that already exists', async function (): Promise { - const tableName = 'table1'; - testContext.apiWrapper.reset(); - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(tableName)); - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - const project = await testUtils.createTestProject(baselines.newProjectFileBaseline); - - should(project.files.length).equal(0, 'There should be no files'); - await projController.addItemPrompt(project, '', templates.script); - should(project.files.length).equal(1, 'File should be successfully added'); - await testUtils.shouldThrowSpecificError(async () => await projController.addItemPrompt(project, '', templates.script), constants.fileAlreadyExists(tableName)); - }); - - it('Should show error if trying to add a folder that already exists', async function (): Promise { - const folderName = 'folder1'; - testContext.apiWrapper.reset(); - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(folderName)); - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - const project = await testUtils.createTestProject(baselines.newProjectFileBaseline); - const projectRoot = new ProjectRootTreeItem(project); - - should(project.files.length).equal(0, 'There should be no other folders'); - await projController.addFolderPrompt(projectRoot); - should(project.files.length).equal(1, 'Folder should be successfully added'); - projController.refreshProjectsTree(); - - await verifyFolderNotAdded(folderName, projController, project, projectRoot); - - // reserved folder names - for (let i in reservedProjectFolders) { - await verifyFolderNotAdded(reservedProjectFolders[i], projController, project, projectRoot); - } - }); - - it('Should be able to add folder with reserved name as long as not at project root', async function (): Promise { - const folderName = 'folder1'; - testContext.apiWrapper.reset(); - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(folderName)); - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - const project = await testUtils.createTestProject(baselines.openProjectFileBaseline); - const projectRoot = new ProjectRootTreeItem(project); - - // make sure it's ok to add these folders if they aren't where the reserved folders are at the root of the project - let node = projectRoot.children.find(c => c.friendlyName === 'Tables'); - for (let i in reservedProjectFolders) { - await verfiyFolderAdded(reservedProjectFolders[i], projController, project, node); - } - }); - - async function verfiyFolderAdded(folderName: string, projController: ProjectsController, project: Project, node: BaseProjectTreeItem): Promise { - const beforeFileCount = project.files.length; - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(folderName)); - await projController.addFolderPrompt(node); - should(project.files.length).equal(beforeFileCount + 1, `File count should be increased by one after adding the folder ${folderName}`); - } - - async function verifyFolderNotAdded(folderName: string, projController: ProjectsController, project: Project, node: BaseProjectTreeItem): Promise { - const beforeFileCount = project.files.length; - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(folderName)); - await testUtils.shouldThrowSpecificError(async () => await projController.addFolderPrompt(node), constants.folderAlreadyExists(folderName)); - should(project.files.length).equal(beforeFileCount, 'File count should be the same as before the folder was attempted to be added'); - } - - it('Should delete nested ProjectEntry from node', async function (): Promise { - let proj = await testUtils.createTestProject(templates.newSqlProjectTemplate); - const setupResult = await setupDeleteExcludeTest(proj); - const scriptEntry = setupResult[0], projTreeRoot = setupResult[1]; - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - - await projController.delete(projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!.children[0] /* LowerFolder */); - - proj = await Project.openProject(proj.projectFilePath); // reload edited sqlproj from disk - - // confirm result - should(proj.files.length).equal(1, 'number of file/folder entries'); // lowerEntry and the contained scripts should be deleted - should(proj.files[0].relativePath).equal('UpperFolder'); - - should(await exists(scriptEntry.fsUri.fsPath)).equal(false, 'script is supposed to be deleted'); - }); - - it('Should exclude nested ProjectEntry from node', async function (): Promise { - let proj = await testUtils.createTestProject(templates.newSqlProjectTemplate); - const setupResult = await setupDeleteExcludeTest(proj); - const scriptEntry = setupResult[0], projTreeRoot = setupResult[1]; - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - - await projController.exclude(projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!.children[0] /* LowerFolder */); - - proj = await Project.openProject(proj.projectFilePath); // reload edited sqlproj from disk - - // confirm result - should(proj.files.length).equal(1, 'number of file/folder entries'); // LowerFolder and the contained scripts should be deleted - should(proj.files[0].relativePath).equal('UpperFolder'); // UpperFolder should still be there - - should(await exists(scriptEntry.fsUri.fsPath)).equal(true, 'script is supposed to still exist on disk'); - }); - }); - - describe('Publishing and script generation', function (): void { - it('Publish dialog should open from ProjectController', async function (): Promise { - let opened = false; - - let publishDialog = TypeMoq.Mock.ofType(PublishDatabaseDialog); - publishDialog.setup(x => x.openDialog()).returns(() => { opened = true; }); - - let projController = TypeMoq.Mock.ofType(ProjectsController); - projController.callBase = true; - projController.setup(x => x.getPublishDialog(TypeMoq.It.isAny())).returns(() => publishDialog.object); - - await projController.object.publishProject(new Project('FakePath')); - should(opened).equal(true); - }); - - it('Callbacks are hooked up and called from Publish dialog', async function (): Promise { - const projPath = path.dirname(await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline)); - await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, projPath); - const proj = new Project(projPath); - - const publishHoller = 'hello from callback for publish()'; - const generateHoller = 'hello from callback for generateScript()'; - const profileHoller = 'hello from callback for readPublishProfile()'; - - let holler = 'nothing'; - - let publishDialog = TypeMoq.Mock.ofType(PublishDatabaseDialog, undefined, undefined, new ApiWrapper(), proj); - publishDialog.callBase = true; - publishDialog.setup(x => x.getConnectionUri()).returns(() => Promise.resolve('fake|connection|uri')); - - let projController = TypeMoq.Mock.ofType(ProjectsController); - projController.callBase = true; - projController.setup(x => x.getPublishDialog(TypeMoq.It.isAny())).returns(() => publishDialog.object); - projController.setup(x => x.executionCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IPublishSettings => true))).returns(() => { - holler = publishHoller; - return Promise.resolve(undefined); - }); - projController.setup(x => x.readPublishProfile(TypeMoq.It.isAny())).returns(() => { - holler = profileHoller; - return Promise.resolve({ - databaseName: '', - sqlCmdVariables: {} - }); - }); - - projController.setup(x => x.executionCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IGenerateScriptSettings => true))).returns(() => { - holler = generateHoller; - return Promise.resolve(undefined); - }); - - let dialog = await projController.object.publishProject(proj); - await dialog.publishClick(); - - should(holler).equal(publishHoller, 'executionCallback() is supposed to have been setup and called for Publish scenario'); - - dialog = await projController.object.publishProject(proj); - await dialog.generateScriptClick(); - - should(holler).equal(generateHoller, 'executionCallback() is supposed to have been setup and called for GenerateScript scenario'); - - dialog = await projController.object.publishProject(proj); - await projController.object.readPublishProfile(vscode.Uri.parse('test')); - - should(holler).equal(profileHoller, 'executionCallback() is supposed to have been setup and called for ReadPublishProfile scenario'); - }); - - it('Should read database name and SQLCMD variables from publish profile', async function (): Promise { - await baselines.loadBaselines(); - let profilePath = await testUtils.createTestFile(baselines.publishProfileBaseline, 'publishProfile.publish.xml'); - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - - let result = await projController.readPublishProfile(vscode.Uri.file(profilePath)); - should(result.databaseName).equal('targetDb'); - should(Object.keys(result.sqlCmdVariables).length).equal(1); - should(result.sqlCmdVariables['ProdDatabaseName']).equal('MyProdDatabase'); - }); - - it('Should copy dacpac to temp folder before publishing', async function (): Promise { - const fakeDacpacContents = 'SwiftFlewHiawathasArrow'; - let postCopyContents = ''; - let builtDacpacPath = ''; - let publishedDacpacPath = ''; - - testContext.dacFxService.setup(x => x.generateDeployScript(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async (p) => { - publishedDacpacPath = p; - postCopyContents = (await fs.readFile(publishedDacpacPath)).toString(); - return Promise.resolve(mockDacFxResult); - }); - - let projController = TypeMoq.Mock.ofType(ProjectsController); - projController.callBase = true; - - projController.setup(x => x.buildProject(TypeMoq.It.isAny())).returns(async () => { - builtDacpacPath = await testUtils.createTestFile(fakeDacpacContents, 'output.dacpac'); - return builtDacpacPath; - }); - - projController.setup(x => x.getDaxFxService()).returns(() => Promise.resolve(testContext.dacFxService.object)); - - await projController.object.executionCallback(new Project(''), { connectionUri: '', databaseName: '' }); - - should(builtDacpacPath).not.equal('', 'built dacpac path should be set'); - should(publishedDacpacPath).not.equal('', 'published dacpac path should be set'); - should(builtDacpacPath).not.equal(publishedDacpacPath, 'built and published dacpac paths should be different'); - should(postCopyContents).equal(fakeDacpacContents, 'contents of built and published dacpacs should match'); + should(await exists(sqlProjPath + '_backup')).equal(true); // backup file should be generated before the project is updated + should(project.importedTargets.length).equal(3); // additional target added by updateProjectForRoundTrip method }); }); }); -describe('ProjectsController: import operations', function (): void { - it('Should create list of all files and folders correctly', async function (): Promise { - const testFolderPath = await testUtils.createDummyFileStructure(); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - const fileList = await projController.generateList(testFolderPath); - - should(fileList.length).equal(15); // Parent folder + 2 files under parent folder + 2 directories with 5 files each - }); - - it('Should error out for inaccessible path', async function (): Promise { - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - let testFolderPath = await testUtils.generateTestFolderPath(); - testFolderPath += '_nonexistentFolder'; // Modify folder path to point to a nonexistent location - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - - await testUtils.shouldThrowSpecificError(async () => await projController.generateList(testFolderPath), constants.cannotResolvePath(testFolderPath)); - }); - - it('Should show error when no project name provided', async function (): Promise { - for (const name of ['', ' ', undefined]) { - testContext.apiWrapper.reset(); - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(name)); - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - await testUtils.shouldThrowSpecificError(async () => await projController.importNewDatabaseProject({ connectionProfile: mockConnectionProfile }), constants.projectNameRequired, `case: '${name}'`); - } - }); - - it('Should show error when no target information provided', async function (): Promise { - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('MyProjectName')); - testContext.apiWrapper.setup(x => x.showOpenDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve([vscode.Uri.file('fakePath')])); - testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - await testUtils.shouldThrowSpecificError(async () => await projController.importNewDatabaseProject({ connectionProfile: mockConnectionProfile }), constants.extractTargetRequired); - }); - - it('Should show error when no location provided with ExtractTarget = File', async function (): Promise { - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('MyProjectName')); - testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ label: constants.file })); - testContext.apiWrapper.setup(x => x.showSaveDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - await testUtils.shouldThrowSpecificError(async () => await projController.importNewDatabaseProject({ connectionProfile: mockConnectionProfile }), constants.projectLocationRequired); - }); - - it('Should show error when no location provided with ExtractTarget = SchemaObjectType', async function (): Promise { - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('MyProjectName')); - testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ label: constants.schemaObjectType })); - testContext.apiWrapper.setup(x => x.showOpenDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); - testContext.apiWrapper.setup(x => x.workspaceFolders()).returns(() => undefined); - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - await testUtils.shouldThrowSpecificError(async () => await projController.importNewDatabaseProject({ connectionProfile: mockConnectionProfile }), constants.projectLocationRequired); - }); - - it('Should set model filePath correctly for ExtractType = File and not-File.', async function (): Promise { - const projectName = 'MyProjectName'; - let folderPath = await testUtils.generateTestFolderPath(); - - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(projectName)); - testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ label: constants.file })); - testContext.apiWrapper.setup(x => x.showOpenDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve([vscode.Uri.file(folderPath)])); - - let importPath; - - let projController = TypeMoq.Mock.ofType(ProjectsController, undefined, undefined, testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - projController.callBase = true; - - projController.setup(x => x.importApiCall(TypeMoq.It.isAny())).returns(async (model) => { importPath = model.filePath; }); - - await projController.object.importNewDatabaseProject({ connectionProfile: mockConnectionProfile }); - should(importPath).equal(vscode.Uri.file(path.join(folderPath, projectName, projectName + '.sql')).fsPath, `model.filePath should be set to a specific file for ExtractTarget === file, but was ${importPath}`); - - // reset for counter-test - importPath = undefined; - folderPath = await testUtils.generateTestFolderPath(); - testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ label: constants.schemaObjectType })); - - await projController.object.importNewDatabaseProject({ connectionProfile: mockConnectionProfile }); - should(importPath).equal(vscode.Uri.file(path.join(folderPath, projectName)).fsPath, `model.filePath should be set to a folder for ExtractTarget !== file, but was ${importPath}`); - }); - - it('Should establish Import context correctly for ObjectExplorer and palette launch points', async function (): Promise { - // test welcome button and palette launch points (context-less) - let mockDbSelection = 'FakeDatabase'; - - testContext.apiWrapper.setup(x => x.listDatabases(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); - testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ label: mockDbSelection })); - testContext.apiWrapper.setup(x => x.openConnectionDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ - providerName: 'MSSQL', - connectionId: 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575', - options: {} - })); - - let projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - - let result = await projController.getModelFromContext(undefined); - - should(result).deepEqual({database: mockDbSelection, serverId: 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575'}); - - // test launch via Object Explorer context - testContext.apiWrapper.reset(); - result = await projController.getModelFromContext(mockConnectionProfile); - should(result).deepEqual({database: 'My Database', serverId: 'My Id'}); - }); -}); - -describe('ProjectsController: add database reference operations', function (): void { - it('Should show error when no reference type is selected', async function (): Promise { - testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - await testUtils.shouldThrowSpecificError(async () => await projController.addDatabaseReference(new Project('FakePath')), constants.databaseReferenceTypeRequired); - }); - - it('Should show error when no file is selected', async function (): Promise { - testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ label: constants.dacpac })); - testContext.apiWrapper.setup(x => x.showOpenDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - await testUtils.shouldThrowSpecificError(async () => await projController.addDatabaseReference(new Project('FakePath')), constants.dacpacFileLocationRequired); - }); - - it('Should show error when no database name is provided', async function (): Promise { - testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ label: constants.dacpac })); - testContext.apiWrapper.setup(x => x.showOpenDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve([vscode.Uri.file('FakePath')])); - testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve({ label: constants.databaseReferenceDifferentDabaseSameServer })); - testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); - testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - await testUtils.shouldThrowSpecificError(async () => await projController.addDatabaseReference(new Project('FakePath')), constants.databaseNameRequired); - }); - - it('Should return the correct system database', async function (): Promise { - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - const projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); - const project = await Project.openProject(projFilePath); - - testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ label: constants.master })); - let systemDb = await projController.getSystemDatabaseName(project); - should.equal(systemDb, SystemDatabase.master); - - testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ label: constants.msdb })); - systemDb = await projController.getSystemDatabaseName(project); - should.equal(systemDb, SystemDatabase.msdb); - - testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); - await testUtils.shouldThrowSpecificError(async () => await projController.getSystemDatabaseName(project), constants.systemDatabaseReferenceRequired); - }); -}); - -describe('ProjectsController: round trip feature with SSDT', function (): void { - it('Should show warning message for SSDT project opened in Azure Data Studio', async function (): Promise { - testContext.apiWrapper.setup(x => x.showWarningMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - // setup test files - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); - await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - - await testUtils.shouldThrowSpecificError(async () => await projController.openProject(vscode.Uri.file(sqlProjPath)), constants.updateProjectForRoundTrip); - }); - - it('Should not show warning message for non-SSDT projects that have the additional information for Build', async function (): Promise { - testContext.apiWrapper.setup(x => x.showWarningMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); - - // setup test files - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline, folderPath); - await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - - const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); // no error thrown - - should(project.importedTargets.length).equal(3); // additional target should exist by default - }); - - it('Should not update project and no backup file should be created when update to project is rejected', async function (): Promise { - testContext.apiWrapper.setup(x => x.showWarningMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(constants.noString)); - - // setup test files - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); - await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - - const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); - - should(await exists(sqlProjPath + '_backup')).equal(false); // backup file should not be generated - should(project.importedTargets.length).equal(2); // additional target should not be added by updateProjectForRoundTrip method - }); - - it('Should load Project and associated import targets when update to project is accepted', async function (): Promise { - testContext.apiWrapper.setup(x => x.showWarningMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(constants.yesString)); - - // setup test files - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); - await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); - - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); - - const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); - - should(await exists(sqlProjPath + '_backup')).equal(true); // backup file should be generated before the project is updated - should(project.importedTargets.length).equal(3); // additional target added by updateProjectForRoundTrip method - }); -}); async function setupDeleteExcludeTest(proj: Project): Promise<[ProjectEntry, ProjectRootTreeItem]> { await proj.addFolderItem('UpperFolder'); @@ -557,8 +585,7 @@ async function setupDeleteExcludeTest(proj: Project): Promise<[ProjectEntry, Pro await proj.addScriptItem('UpperFolder/LowerFolder/someOtherScript.sql', 'Also not a real script'); const projTreeRoot = new ProjectRootTreeItem(proj); - - testContext.apiWrapper.setup(x => x.showWarningMessageOptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(constants.yesString)); + sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.yesString)); // confirm setup should(proj.files.length).equal(4, 'number of file/folder entries'); diff --git a/extensions/sql-database-projects/src/test/publishDatabaseDialog.test.ts b/extensions/sql-database-projects/src/test/publishDatabaseDialog.test.ts index 0eabb4b3d4..035a97f39d 100644 --- a/extensions/sql-database-projects/src/test/publishDatabaseDialog.test.ts +++ b/extensions/sql-database-projects/src/test/publishDatabaseDialog.test.ts @@ -16,48 +16,40 @@ import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog'; import { Project } from '../models/project'; import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProjectTreeViewProvider'; import { ProjectsController } from '../controllers/projectController'; -import { createContext, TestContext } from './testContext'; import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSettings'; - -let testContext: TestContext; - describe.skip('Publish Database Dialog', () => { before(async function (): Promise { await templates.loadTemplates(path.join(__dirname, '..', '..', 'resources', 'templates')); await baselines.loadBaselines(); }); - beforeEach(function (): void { - testContext = createContext(); - }); - it('Should open dialog successfully ', async function (): Promise { - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`); const projFilePath = await projController.createNewProject('TestProjectName', vscode.Uri.file(projFileDir), true, 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575'); const project = new Project(projFilePath); - const publishDatabaseDialog = new PublishDatabaseDialog(testContext.apiWrapper.object, project); + const publishDatabaseDialog = new PublishDatabaseDialog(project); publishDatabaseDialog.openDialog(); should.notEqual(publishDatabaseDialog.publishTab, undefined); }); it('Should create default database name correctly ', async function (): Promise { - const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); const projFolder = `TestProject_${new Date().getTime()}`; const projFileDir = path.join(os.tmpdir(), projFolder); const projFilePath = await projController.createNewProject('TestProjectName', vscode.Uri.file(projFileDir), true, 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575'); const project = new Project(projFilePath); - const publishDatabaseDialog = new PublishDatabaseDialog(testContext.apiWrapper.object, project); + const publishDatabaseDialog = new PublishDatabaseDialog(project); should.equal(publishDatabaseDialog.getDefaultDatabaseName(), project.projectFileName); }); it('Should include all info in publish profile', async function (): Promise { const proj = await testUtils.createTestProject(baselines.openProjectFileBaseline); - const dialog = TypeMoq.Mock.ofType(PublishDatabaseDialog, undefined, undefined, testContext.apiWrapper.object, proj); + const dialog = TypeMoq.Mock.ofType(PublishDatabaseDialog, undefined, undefined, proj); dialog.setup(x => x.getConnectionUri()).returns(() => { return Promise.resolve('Mock|Connection|Uri'); }); dialog.setup(x => x.getTargetDatabaseName()).returns(() => 'MockDatabaseName'); dialog.callBase = true; diff --git a/extensions/sql-database-projects/src/test/testContext.ts b/extensions/sql-database-projects/src/test/testContext.ts index e106f14fea..bd5275882b 100644 --- a/extensions/sql-database-projects/src/test/testContext.ts +++ b/extensions/sql-database-projects/src/test/testContext.ts @@ -8,10 +8,8 @@ import * as azdata from 'azdata'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; import * as mssql from '../../../mssql/src/mssql'; -import { ApiWrapper } from '../common/apiWrapper'; export interface TestContext { - apiWrapper: TypeMoq.IMock; context: vscode.ExtensionContext; dacFxService: TypeMoq.IMock; } @@ -37,7 +35,6 @@ export function createContext(): TestContext { let extensionPath = path.join(__dirname, '..', '..'); return { - apiWrapper: TypeMoq.Mock.ofType(ApiWrapper), context: { subscriptions: [], workspaceState: { diff --git a/extensions/sql-database-projects/yarn.lock b/extensions/sql-database-projects/yarn.lock index b28f440114..5841c48c19 100644 --- a/extensions/sql-database-projects/yarn.lock +++ b/extensions/sql-database-projects/yarn.lock @@ -226,6 +226,54 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" +"@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.7.2": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d" + integrity sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/formatio@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089" + integrity sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ== + dependencies: + "@sinonjs/commons" "^1" + "@sinonjs/samsam" "^5.0.2" + +"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.0.3.tgz#86f21bdb3d52480faf0892a480c9906aa5a52938" + integrity sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ== + dependencies: + "@sinonjs/commons" "^1.6.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" + integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== + +"@types/sinon@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.4.tgz#e934f904606632287a6e7f7ab0ce3f08a0dad4b1" + integrity sha512-sJmb32asJZY6Z2u09bl0G2wglSxDlROlAejCjsnor+LzBMz17gu8IU7vKC/vWDnv9zEq2wqADHVXFjf4eE8Gdw== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinonjs__fake-timers@*": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" + integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== + "@types/xml-formatter@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/xml-formatter/-/xml-formatter-1.1.0.tgz#f7cde70ec33d7b044029b6b6c2f6e69d270ced63" @@ -407,6 +455,11 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q== +diff@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -554,6 +607,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + istanbul-lib-coverage@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" @@ -637,6 +695,16 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +just-extend@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4" + integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA== + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + lodash@^4.16.4, lodash@^4.17.13, lodash@^4.17.4: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" @@ -756,6 +824,17 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +nise@^4.0.1: + version "4.0.4" + resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.4.tgz#d73dea3e5731e6561992b8f570be9e363c4512dd" + integrity sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@sinonjs/fake-timers" "^6.0.0" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + path-to-regexp "^1.7.0" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -773,6 +852,13 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + picomatch@^2.0.5, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" @@ -883,6 +969,19 @@ should@^13.2.1: should-type-adaptors "^1.0.1" should-util "^1.0.0" +sinon@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.0.2.tgz#b9017e24633f4b1c98dfb6e784a5f0509f5fd85d" + integrity sha512-0uF8Q/QHkizNUmbK3LRFqx5cpTttEVXudywY9Uwzy8bTfZUhljZ7ARzSxnRHWYWtVTeh4Cw+tTb3iU21FQVO9A== + dependencies: + "@sinonjs/commons" "^1.7.2" + "@sinonjs/fake-timers" "^6.0.1" + "@sinonjs/formatio" "^5.0.1" + "@sinonjs/samsam" "^5.0.3" + diff "^4.0.2" + nise "^4.0.1" + supports-color "^7.1.0" + source-map@^0.5.0: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -974,6 +1073,11 @@ tsutils@^2.29.0: dependencies: tslib "^1.8.1" +type-detect@4.0.8, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + typemoq@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8"