Database projects deploy (#10417)

* Hooking up deployment of SQL projects to the project build functionality and database selection UI
* Adding ADS-side plumbing for sqlcmdvars
This commit is contained in:
Benjin Dubishar
2020-05-26 14:28:09 -07:00
committed by GitHub
parent 690e3c582c
commit f0d86f8acb
14 changed files with 340 additions and 88 deletions

View File

@@ -10,7 +10,8 @@ const localize = nls.loadMessageBundle();
// Placeholder values
export const dataSourcesFileName = 'datasources.json';
export const sqlprojExtension = '.sqlproj';
export const initialCatalogSetting = 'Initial Catalog';
// Commands
export const schemaCompareExtensionId = 'microsoft.schema-compare';
export const sqlDatabaseProjectExtensionId = 'microsoft.sql-database-projects';
export const mssqlExtensionId = 'microsoft.mssql';
@@ -59,6 +60,8 @@ export const buildDacpacNotFound = localize('buildDacpacNotFound', "Dacpac creat
export function projectAlreadyOpened(path: string) { return localize('projectAlreadyOpened', "Project '{0}' is already opened.", path); }
export function projectAlreadyExists(name: string, path: string) { return localize('projectAlreadyExists', "A project named {0} already exists in {1}.", name, path); }
export function mssqlNotFound(mssqlConfigDir: string) { return localize('mssqlNotFound', "Could not get mssql extension's install location at {0}", mssqlConfigDir); }
export function projBuildFailed(errorMessage: string) { return localize('projBuildFailed', "Build failed. Check output pane for more details. {0}", errorMessage); }
export function unexpectedProjectContext(uri: string) { return localize('unexpectedProjectContext', "Unable to establish project context. Command invoked from unexpected location: {0}", uri); }
// Project script types
@@ -73,3 +76,10 @@ export const ItemGroup = 'ItemGroup';
export const Build = 'Build';
export const Folder = 'Folder';
export const Include = 'Include';
// SQL connection string components
export const initialCatalogSetting = 'Initial Catalog';
export const dataSourceSetting = 'Data Source';
export const integratedSecuritySetting = 'Integrated Security';
export const userIdSetting = 'User ID';
export const passwordSetting = 'Password';

View File

@@ -50,7 +50,7 @@ export default class MainController implements Disposable {
this.apiWrapper.registerCommand('sqlDatabaseProjects.properties', async (node: BaseProjectTreeItem) => { await this.apiWrapper.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.deploy', async (node: BaseProjectTreeItem) => { await this.projectsController.deploy(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.deploy', async (node: BaseProjectTreeItem) => { await this.projectsController.deployProject(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.schemaCompare', async (node: BaseProjectTreeItem) => { await this.projectsController.schemaCompare(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.import', async (node: BaseProjectTreeItem) => { await this.projectsController.import(node); });

View File

@@ -3,22 +3,25 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as constants from '../common/constants';
import * as dataSources from '../models/dataSources/dataSources';
import * as mssql from '../../../mssql';
import * as path from 'path';
import * as utils from '../common/utils';
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
import * as templates from '../templates/templates';
import { Uri, QuickPickItem } from 'vscode';
import { TaskExecutionMode } from 'azdata';
import { promises as fs } from 'fs';
import { Uri, QuickPickItem, extensions, Extension } from 'vscode';
import { ApiWrapper } from '../common/apiWrapper';
import { DeployDatabaseDialog } from '../dialogs/deployDatabaseDialog';
import { Project } from '../models/project';
import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider';
import { promises as fs } from 'fs';
import { FolderNode } from '../models/tree/fileFolderTreeItem';
import { IDeploymentProfile, IGenerateScriptProfile } from '../models/IDeploymentProfile';
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
import { ProjectRootTreeItem } from '../models/tree/projectTreeItem';
import { FolderNode } from '../models/tree/fileFolderTreeItem';
import { DeployDatabaseDialog } from '../dialogs/deployDatabaseDialog';
import { NetCoreTool, DotNetCommandOptions } from '../tools/netcoreTool';
import { BuildHelper } from '../tools/buildHelper';
@@ -114,28 +117,82 @@ export class ProjectsController {
}
public closeProject(treeNode: BaseProjectTreeItem) {
const project = this.getProjectContextFromTreeNode(treeNode);
const project = ProjectsController.getProjectFromContext(treeNode);
this.projects = this.projects.filter((e) => { return e !== project; });
this.refreshProjectsTree();
}
public async buildProject(treeNode: BaseProjectTreeItem): Promise<void> {
/**
* Builds a project, producing a dacpac
* @param treeNode a treeItem in a project's hierarchy, to be used to obtain a Project
* @returns path of the built dacpac
*/
public async buildProject(treeNode: BaseProjectTreeItem): Promise<string>;
/**
* Builds a project, producing a dacpac
* @param project Project to be built
* @returns path of the built dacpac
*/
public async buildProject(project: Project): Promise<string>;
public async buildProject(context: Project | BaseProjectTreeItem): Promise<string | undefined> {
const project: Project = ProjectsController.getProjectFromContext(context);
// Check mssql extension for project dlls (tracking issue #10273)
await this.buildHelper.createBuildDirFolder();
const project = this.getProjectContextFromTreeNode(treeNode);
const options: DotNetCommandOptions = {
commandTitle: 'Build',
workingDirectory: project.projectFolderPath,
argument: this.buildHelper.constructBuildArguments(project.projectFilePath, this.buildHelper.extensionBuildDirPath)
};
await this.netCoreTool.runDotnetCommand(options);
try {
await this.netCoreTool.runDotnetCommand(options);
return path.join(project.projectFolderPath, 'bin', 'Debug', `${project.projectFileName}.dacpac`);
}
catch (err) {
this.apiWrapper.showErrorMessage(constants.projBuildFailed(utils.getErrorMessage(err)));
return undefined;
}
}
public deploy(treeNode: BaseProjectTreeItem): void {
const project = this.getProjectContextFromTreeNode(treeNode);
const deployDatabaseDialog = new DeployDatabaseDialog(this.apiWrapper, project);
/**
* Builds and deploys a project
* @param treeNode a treeItem in a project's hierarchy, to be used to obtain a Project
*/
public async deployProject(treeNode: BaseProjectTreeItem): Promise<DeployDatabaseDialog>;
/**
* Builds and deploys a project
* @param project Project to be built and deployed
*/
public async deployProject(project: Project): Promise<DeployDatabaseDialog>;
public async deployProject(context: Project | BaseProjectTreeItem): Promise<DeployDatabaseDialog> {
const project: Project = ProjectsController.getProjectFromContext(context);
let deployDatabaseDialog = this.getDeployDialog(project);
deployDatabaseDialog.deploy = async (proj, prof) => await this.executionCallback(proj, prof);
deployDatabaseDialog.generateScript = async (proj, prof) => await this.executionCallback(proj, prof);
deployDatabaseDialog.openDialog();
return deployDatabaseDialog;
}
public async executionCallback(project: Project, profile: IDeploymentProfile | IGenerateScriptProfile): Promise<mssql.DacFxResult | undefined> {
const dacpacPath = await this.buildProject(project);
if (!dacpacPath) {
return undefined; // buildProject() handles displaying the error
}
const dacFxService = await ProjectsController.getDaxFxService();
if (profile as IDeploymentProfile) {
return await dacFxService.deployDacpac(dacpacPath, profile.databaseName, (<IDeploymentProfile>profile).upgradeExisting, profile.connectionUri, TaskExecutionMode.execute, profile.sqlCmdVariables);
}
else {
return await dacFxService.generateDeployScript(dacpacPath, profile.databaseName, profile.connectionUri, TaskExecutionMode.execute, profile.sqlCmdVariables);
}
}
public async schemaCompare(treeNode: BaseProjectTreeItem): Promise<void> {
@@ -145,7 +202,7 @@ export class ProjectsController {
await this.buildProject(treeNode);
// start schema compare with the dacpac produced from build
const project = this.getProjectContextFromTreeNode(treeNode);
const project = ProjectsController.getProjectFromContext(treeNode);
const dacpacPath = path.join(project.projectFolderPath, 'bin', 'Debug', `${project.projectFileName}.dacpac`);
// check that dacpac exists
@@ -160,12 +217,12 @@ export class ProjectsController {
}
public async import(treeNode: BaseProjectTreeItem) {
const project = this.getProjectContextFromTreeNode(treeNode);
const project = ProjectsController.getProjectFromContext(treeNode);
await this.apiWrapper.showErrorMessage(`Import not yet implemented: ${project.projectFilePath}`); // TODO
}
public async addFolderPrompt(treeNode: BaseProjectTreeItem) {
const project = this.getProjectContextFromTreeNode(treeNode);
const project = ProjectsController.getProjectFromContext(treeNode);
const newFolderName = await this.promptForNewObjectName(new templates.ProjectScriptType(templates.folder, constants.folderFriendlyName, ''), project);
if (!newFolderName) {
@@ -180,7 +237,7 @@ export class ProjectsController {
}
public async addItemPromptFromNode(treeNode: BaseProjectTreeItem, itemTypeName?: string) {
await this.addItemPrompt(this.getProjectContextFromTreeNode(treeNode), this.getRelativePath(treeNode), itemTypeName);
await this.addItemPrompt(ProjectsController.getProjectFromContext(treeNode), this.getRelativePath(treeNode), itemTypeName);
}
public async addItemPrompt(project: Project, relativePath: string, itemTypeName?: string) {
@@ -223,6 +280,30 @@ export class ProjectsController {
//#region Helper methods
public getDeployDialog(project: Project): DeployDatabaseDialog {
return new DeployDatabaseDialog(this.apiWrapper, project);
}
private static getProjectFromContext(context: Project | BaseProjectTreeItem) {
if (context instanceof Project) {
return context;
}
if (context.root instanceof ProjectRootTreeItem) {
return (<ProjectRootTreeItem>context.root).project;
}
else {
throw new Error(constants.unexpectedProjectContext(context.uri.path));
}
}
private static async getDaxFxService(): Promise<mssql.IDacFxService> {
const ext: Extension<any> = extensions.getExtension(mssql.extension.name)!;
await ext.activate();
return (ext.exports as mssql.IExtension).dacFx;
}
private macroExpansion(template: string, macroDict: Record<string, string>): string {
const macroIndicator = '@@';
let output = template;
@@ -239,20 +320,6 @@ export class ProjectsController {
return output;
}
private getProjectContextFromTreeNode(treeNode: BaseProjectTreeItem): Project {
if (!treeNode) {
// TODO: prompt for which (currently-open) project when invoked via command pallet
throw new Error('TODO: prompt for which project when invoked via command pallet');
}
if (treeNode.root instanceof ProjectRootTreeItem) {
return (treeNode.root as ProjectRootTreeItem).project;
}
else {
throw new Error('Unable to establish project context. Command invoked from unexpected location: ' + treeNode.uri.path);
}
}
private async promptForNewObjectName(itemType: templates.ProjectScriptType, _project: Project): Promise<string | undefined> {
// TODO: ask project for suggested name that doesn't conflict
const suggestedName = itemType.friendlyName.replace(new RegExp('\s', 'g'), '') + '1';

View File

@@ -4,14 +4,16 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as constants from '../common/constants';
import { Project } from '../models/project';
import { DataSource } from '../models/dataSources/dataSources';
import { SqlConnectionDataSource } from '../models/dataSources/sqlConnectionStringSource';
import { ApiWrapper } from '../common/apiWrapper';
import { IDeploymentProfile, IGenerateScriptProfile } from '../models/IDeploymentProfile';
interface DataSourceDropdownValue extends azdata.CategoryValue {
dataSource: DataSource;
dataSource: SqlConnectionDataSource;
database: string;
}
@@ -30,6 +32,11 @@ export class DeployDatabaseDialog {
private connection: azdata.connection.Connection | undefined;
private connectionIsDataSource: boolean | undefined;
private toDispose: vscode.Disposable[] = [];
public deploy: ((proj: Project, profile: IDeploymentProfile) => any) | undefined;
public generateScript: ((proj: Project, profile: IGenerateScriptProfile) => any) | undefined;
constructor(private apiWrapper: ApiWrapper, private project: Project) {
this.dialog = azdata.window.createModelViewDialog(constants.deployDialogName);
this.deployTab = azdata.window.createTab(constants.deployDialogName);
@@ -39,12 +46,12 @@ export class DeployDatabaseDialog {
this.initializeDialog();
this.dialog.okButton.label = constants.deployDialogOkButtonText;
this.dialog.okButton.enabled = false;
this.dialog.okButton.onClick(async () => await this.deploy());
this.toDispose.push(this.dialog.okButton.onClick(async () => await this.deployClick()));
this.dialog.cancelButton.label = constants.cancelButtonText;
let generateScriptButton: azdata.window.Button = azdata.window.createButton(constants.generateScriptButtonText);
generateScriptButton.onClick(async () => await this.generateScript());
this.toDispose.push(generateScriptButton.onClick(async () => await this.generateScriptClick()));
generateScriptButton.enabled = false;
this.dialog.customButtons = [];
@@ -53,6 +60,9 @@ export class DeployDatabaseDialog {
azdata.window.openDialog(this.dialog);
}
private dispose(): void {
this.toDispose.forEach(disposable => disposable.dispose());
}
private initializeDialog(): void {
this.initializeDeployTab();
@@ -104,17 +114,77 @@ export class DeployDatabaseDialog {
});
}
private async deploy(): Promise<void> {
// TODO: hook up with build and deploy
public async getConnectionUri(): Promise<string> {
// if target connection is a data source, have to check if already connected or if connection dialog needs to be opened
let connId: string;
if (this.dataSourcesRadioButton?.checked) {
const dataSource = (this.dataSourcesDropDown!.value! as DataSourceDropdownValue).dataSource;
const connProfile: azdata.IConnectionProfile = {
serverName: dataSource.server,
databaseName: dataSource.database,
connectionName: dataSource.name,
userName: dataSource.username,
password: dataSource.password,
authenticationType: dataSource.integratedSecurity ? 'Integrated' : 'SqlAuth',
savePassword: false,
providerName: 'MSSQL',
saveProfile: true,
id: dataSource.name + '-dataSource',
options: []
};
if (dataSource.integratedSecurity) {
connId = (await azdata.connection.connect(connProfile, false, false)).connectionId;
}
else {
connId = (await azdata.connection.openConnectionDialog(undefined, connProfile)).connectionId;
}
}
else {
if (!this.connection) {
throw new Error('Connection not defined.');
}
connId = this.connection?.connectionId;
}
return await azdata.connection.getUriForConnection(connId);
}
private async generateScript(): Promise<void> {
// TODO: hook up with build and generate script
// if target connection is a data source, have to check if already connected or if connection dialog needs to be opened
public async deployClick(): Promise<void> {
const profile: IDeploymentProfile = {
databaseName: this.getTargetDatabaseName(),
upgradeExisting: true,
connectionUri: await this.getConnectionUri(),
sqlCmdVariables: this.project.sqlCmdVariables
};
await this.deploy!(this.project, profile);
this.dispose();
azdata.window.closeDialog(this.dialog);
}
public async generateScriptClick(): Promise<void> {
const profile: IGenerateScriptProfile = {
databaseName: this.getTargetDatabaseName(),
connectionUri: await this.getConnectionUri()
};
if (this.generateScript) {
await this.generateScript!(this.project, profile);
}
this.dispose();
azdata.window.closeDialog(this.dialog);
}
private getTargetDatabaseName(): string {
return this.targetDatabaseTextBox?.value ?? '';
}
public getDefaultDatabaseName(): string {
return this.project.projectFileName;
}
@@ -193,13 +263,13 @@ export class DeployDatabaseDialog {
private createDataSourcesDropdown(view: azdata.ModelView): azdata.FormComponent {
let dataSourcesValues: DataSourceDropdownValue[] = [];
this.project.dataSources.forEach(dataSource => {
const dbName: string = (dataSource as SqlConnectionDataSource).getSetting(constants.initialCatalogSetting);
this.project.dataSources.filter(d => d instanceof SqlConnectionDataSource).forEach(dataSource => {
const dbName: string = (dataSource as SqlConnectionDataSource).database;
const displayName: string = `${dataSource.name}`;
dataSourcesValues.push({
displayName: displayName,
name: dataSource.name,
dataSource: dataSource,
dataSource: dataSource as SqlConnectionDataSource,
database: dbName
});
});
@@ -221,7 +291,7 @@ export class DeployDatabaseDialog {
}
private setDatabaseToSelectedDataSourceDatabase(): void {
if ((<DataSourceDropdownValue>this.dataSourcesDropDown!.value).database) {
if ((<DataSourceDropdownValue>this.dataSourcesDropDown!.value)?.database) {
this.targetDatabaseTextBox!.value = (<DataSourceDropdownValue>this.dataSourcesDropDown!.value).database;
}
}

View File

@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface IDeploymentProfile {
databaseName: string;
connectionUri: string;
upgradeExisting: boolean;
sqlCmdVariables?: Record<string, string>;
}
export interface IGenerateScriptProfile {
databaseName: string;
connectionUri: string;
sqlCmdVariables?: Record<string, string>;
}

View File

@@ -25,6 +25,27 @@ export class SqlConnectionDataSource extends DataSource {
return constants.sqlConnectionStringFriendly;
}
public get server(): string {
return this.getSetting(constants.dataSourceSetting);
}
public get database(): string {
return this.getSetting(constants.initialCatalogSetting);
}
public get integratedSecurity(): boolean {
return this.getSetting(constants.integratedSecuritySetting).toLowerCase() === 'true';
}
public get username(): string {
return this.getSetting(constants.userIdSetting);
}
public get password(): string {
// TODO: secure password storage; https://github.com/microsoft/azuredatastudio/issues/10561
return this.getSetting(constants.passwordSetting);
}
constructor(name: string, connectionString: string) {
super(name);
@@ -38,12 +59,12 @@ export class SqlConnectionDataSource extends DataSource {
throw new Error(constants.invalidSqlConnectionString);
}
this.connectionStringComponents[split[0]] = split[1];
this.connectionStringComponents[split[0].toLocaleLowerCase()] = split[1];
}
}
public getSetting(settingName: string): string {
return this.connectionStringComponents[settingName];
return this.connectionStringComponents[settingName.toLocaleLowerCase()];
}
public static fromJson(json: DataSourceJson): SqlConnectionDataSource {

View File

@@ -19,6 +19,7 @@ export class Project {
public projectFileName: string;
public files: ProjectEntry[] = [];
public dataSources: DataSource[] = [];
public sqlCmdVariables: Record<string, string> = {};
public get projectFolderPath() {
return path.dirname(this.projectFilePath);

View File

@@ -22,9 +22,9 @@ describe('Data Sources: DataSource operations', function (): void {
should(dataSourceList[0].name).equal('Test Data Source 1');
should(dataSourceList[0].type).equal(sql.SqlConnectionDataSource.type);
should((dataSourceList[0] as sql.SqlConnectionDataSource).getSetting('Initial Catalog')).equal('testDb');
should((dataSourceList[0] as sql.SqlConnectionDataSource).database).equal('testDb');
should(dataSourceList[1].name).equal('My Other Data Source');
should((dataSourceList[1] as sql.SqlConnectionDataSource).getSetting('Integrated Security')).equal('False');
should((dataSourceList[1] as sql.SqlConnectionDataSource).integratedSecurity).equal(false);
});
});

View File

@@ -19,7 +19,7 @@ describe('Project: sqlproj content operations', function (): void {
});
beforeEach(async () => {
projFilePath = await testUtils.createTestSqlProj(baselines.openProjectFileBaseline);
projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline);
});
it('Should read Project from sqlproj', async function (): Promise<void> {

View File

@@ -17,6 +17,9 @@ import { ProjectsController } from '../controllers/projectController';
import { promises as fs } from 'fs';
import { createContext, TestContext } from './testContext';
import { Project } from '../models/project';
import { DeployDatabaseDialog } from '../dialogs/deployDatabaseDialog';
import { ApiWrapper } from '../common/apiWrapper';
import { IDeploymentProfile, IGenerateScriptProfile } from '../models/IDeploymentProfile';
let testContext: TestContext;
@@ -27,43 +30,99 @@ describe('ProjectsController: project controller operations', function (): void
await baselines.loadBaselines();
});
it('Should create new sqlproj file with correct values', async function (): Promise<void> {
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`);
describe('Project file operations and prompting', function (): void {
it('Should create new sqlproj file with correct values', async function (): Promise<void> {
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`);
const projFilePath = await projController.createNewProject('TestProjectName', vscode.Uri.file(projFileDir), 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575');
const projFilePath = await projController.createNewProject('TestProjectName', vscode.Uri.file(projFileDir), 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575');
let projFileText = (await fs.readFile(projFilePath)).toString();
let projFileText = (await fs.readFile(projFilePath)).toString();
should(projFileText).equal(baselines.newProjectFileBaseline);
});
should(projFileText).equal(baselines.newProjectFileBaseline);
});
it('Should load Project and associated DataSources', async function (): Promise<void> {
// setup test files
const folderPath = await testUtils.generateTestFolderPath();
const sqlProjPath = await testUtils.createTestSqlProj(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));
should(project.files.length).equal(9); // detailed sqlproj tests in their own test file
should(project.dataSources.length).equal(2); // detailed datasources tests in their own test file
});
it('Should return silently when no object name provided', async function (): Promise<void> {
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) => { console.log('we throwin'); throw new Error(s); });
it('Should load Project and associated DataSources', async function (): Promise<void> {
// 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 = 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.');
}
const project = await projController.openProject(vscode.Uri.file(sqlProjPath));
should(project.files.length).equal(9); // detailed sqlproj tests in their own test file
should(project.dataSources.length).equal(2); // detailed datasources tests in their own test file
});
it('Should return silently when no SQL object name provided in prompts', async function (): Promise<void> {
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());
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.');
}
});
});
describe('Deployment and deployment script generation', function (): void {
it('Deploy dialog should open from ProjectController', async function (): Promise<void> {
let opened = false;
let deployDialog = TypeMoq.Mock.ofType(DeployDatabaseDialog);
deployDialog.setup(x => x.openDialog()).returns(() => { opened = true; });
let projController = TypeMoq.Mock.ofType(ProjectsController);
projController.callBase = true;
projController.setup(x => x.getDeployDialog(TypeMoq.It.isAny())).returns(() => deployDialog.object);
await projController.object.deployProject(new Project('FakePath'));
should(opened).equal(true);
});
it('Callbacks are hooked up and called from Deploy dialog', async function (): Promise<void> {
const projPath = path.dirname(await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline));
await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, projPath);
const proj = new Project(projPath);
const deployHoller = 'hello from callback for deploy()';
const generateHoller = 'hello from callback for generateScript()';
let holler = 'nothing';
let deployDialog = TypeMoq.Mock.ofType(DeployDatabaseDialog, undefined, undefined, new ApiWrapper(), proj);
deployDialog.callBase = true;
deployDialog.setup(x => x.getConnectionUri()).returns(async () => 'fake|connection|uri');
let projController = TypeMoq.Mock.ofType(ProjectsController);
projController.callBase = true;
projController.setup(x => x.getDeployDialog(TypeMoq.It.isAny())).returns(() => deployDialog.object);
projController.setup(x => x.executionCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IDeploymentProfile => true))).returns(async () => {
holler = deployHoller;
return undefined;
});
projController.setup(x => x.executionCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IGenerateScriptProfile => true))).returns(async () => {
holler = generateHoller;
return undefined;
});
let dialog = await projController.object.deployProject(proj);
await dialog.deployClick();
should(holler).equal(deployHoller, 'executionCallback() is supposed to have been setup and called for Deploy scenario');
dialog = await projController.object.deployProject(proj);
await dialog.generateScriptClick();
should(holler).equal(generateHoller, 'executionCallback() is supposed to have been setup and called for GenerateScript scenario');
});
});
});

View File

@@ -10,6 +10,7 @@ import * as constants from '../common/constants';
import { promises as fs } from 'fs';
import should = require('should');
import { AssertionError } from 'assert';
import { Project } from '../models/project';
export async function shouldThrowSpecificError(block: Function, expectedMessage: string, details?: string) {
let succeeded = false;
@@ -26,10 +27,14 @@ export async function shouldThrowSpecificError(block: Function, expectedMessage:
}
}
export async function createTestSqlProj(contents: string, folderPath?: string): Promise<string> {
export async function createTestSqlProjFile(contents: string, folderPath?: string): Promise<string> {
return await createTestFile(contents, 'TestProject.sqlproj', folderPath);
}
export async function createTestProject(contents: string, folderPath?: string): Promise<Project> {
return new Project(await createTestSqlProjFile(contents, folderPath));
}
export async function createTestDataSources(contents: string, folderPath?: string): Promise<string> {
return await createTestFile(contents, constants.dataSourcesFileName, folderPath);
}