diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 7ec68cbefd..0ec5eeef67 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -77,7 +77,7 @@ export const selectConnectionRadioButtonsTitle = localize('selectconnectionRadio export const dataSourceDropdownTitle = localize('dataSourceDropdownTitle', "Data source"); export const noDataSourcesText = localize('noDataSourcesText', "No data sources in this project"); export const loadProfileButtonText = localize('loadProfileButtonText', "Load Profile..."); -export const profileWarningText = localize('profileWarningText', "⚠Warning: Only database name and SQLCMD variables are able to be loaded from a profile at this time"); +export const profileWarningText = localize('profileWarningText', "⚠ Warning: Only SQL Login and Integrated Authentication connection strings, database name, and SQLCMD variables are able to be loaded from a profile at this time"); export const sqlCmdTableLabel = localize('sqlCmdTableLabel', "SQLCMD Variables"); export const sqlCmdVariableColumn = localize('sqlCmdVariableColumn', "Variable"); export const sqlCmdValueColumn = localize('sqlCmdValueColumn', "Value"); @@ -107,7 +107,6 @@ export const databaseNameRequired = localize('databaseNameRequired', "Database n export const invalidDataSchemaProvider = localize('invalidDataSchemaProvider', "Invalid DSP in .sqlproj file"); export const invalidDatabaseReference = localize('invalidDatabaseReference', "Invalid database reference in .sqlproj file"); export const databaseSelectionRequired = localize('databaseSelectionRequired', "Database selection is required to import a project"); -export const unableToCreatePublishConnection = localize('unableToCreatePublishConnection', "Unable to construct connection"); export const databaseReferenceAlreadyExists = localize('databaseReferenceAlreadyExists', "A reference to this database already exists in this project"); 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); } @@ -116,6 +115,7 @@ export function cannotResolvePath(path: string) { return localize('cannotResolve export function fileAlreadyExists(filename: string) { return localize('fileAlreadyExists', "A file with the name '{0}' already exists on disk at this location. Please choose another name.", filename); } export function folderAlreadyExists(filename: string) { return localize('folderAlreadyExists', "A folder with the name '{0}' already exists on disk at this location. Please choose another name.", filename); } export function invalidInput(input: string) { return localize('invalidInput', "Invalid input: {0}", input); } +export function unableToCreatePublishConnection(input: string) { return localize('unableToCreatePublishConnection', "Unable to construct connection: {0}", input); } 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); } @@ -183,6 +183,7 @@ export const All = 'All'; // Profile XML names export const targetDatabaseName = 'TargetDatabaseName'; +export const targetConnectionString = 'TargetConnectionString'; // SQL connection string components export const initialCatalogSetting = 'Initial Catalog'; diff --git a/extensions/sql-database-projects/src/common/utils.ts b/extensions/sql-database-projects/src/common/utils.ts index 3815472618..cf2ae2354e 100644 --- a/extensions/sql-database-projects/src/common/utils.ts +++ b/extensions/sql-database-projects/src/common/utils.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import * as os from 'os'; -import * as constants from '../common/constants'; +import * as constants from './constants'; import { promises as fs } from 'fs'; /** diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index a779395aeb..2bbddb52fa 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -11,7 +11,6 @@ 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 * as xmldom from 'xmldom'; import * as vscode from 'vscode'; import * as azdata from 'azdata'; @@ -20,12 +19,13 @@ import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog'; import { Project, DatabaseReferenceLocation, SystemDatabase, TargetPlatform, ProjectEntry, reservedProjectFolders } from '../models/project'; import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider'; import { FolderNode, FileNode } from '../models/tree/fileFolderTreeItem'; -import { IPublishSettings, IGenerateScriptSettings, PublishProfile } from '../models/IPublishSettings'; +import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSettings'; import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; import { ProjectRootTreeItem } from '../models/tree/projectTreeItem'; import { ImportDataModel } from '../models/api/import'; import { NetCoreTool, DotNetCommandOptions } from '../tools/netcoreTool'; import { BuildHelper } from '../tools/buildHelper'; +import { PublishProfile, load } from '../models/publishProfile/publishProfile'; /** * Controller for managing project lifecycle @@ -199,7 +199,7 @@ export class ProjectsController { publishDatabaseDialog.publish = async (proj, prof) => await this.executionCallback(proj, prof); publishDatabaseDialog.generateScript = async (proj, prof) => await this.executionCallback(proj, prof); - publishDatabaseDialog.readPublishProfile = async (profileUri) => await this.readPublishProfile(profileUri); + publishDatabaseDialog.readPublishProfile = async (profileUri) => await this.readPublishProfileCallback(profileUri); publishDatabaseDialog.openDialog(); @@ -227,25 +227,9 @@ export class ProjectsController { } } - public async readPublishProfile(profileUri: vscode.Uri): Promise { - const profileText = await fs.readFile(profileUri.fsPath); - const profileXmlDoc = new xmldom.DOMParser().parseFromString(profileText.toString()); - - // read target database name - let targetDbName: string = ''; - let targetDatabaseNameCount = profileXmlDoc.documentElement.getElementsByTagName(constants.targetDatabaseName).length; - if (targetDatabaseNameCount > 0) { - // if there is more than one TargetDatabaseName nodes, SSDT uses the name in the last one so we'll do the same here - targetDbName = profileXmlDoc.documentElement.getElementsByTagName(constants.targetDatabaseName)[targetDatabaseNameCount - 1].textContent; - } - - // get all SQLCMD variables to include from the profile - let sqlCmdVariables = utils.readSqlCmdVariables(profileXmlDoc); - - return { - databaseName: targetDbName, - sqlCmdVariables: sqlCmdVariables - }; + public async readPublishProfileCallback(profileUri: vscode.Uri): Promise { + const profile = await load(profileUri); + return profile; } public async schemaCompare(treeNode: BaseProjectTreeItem): Promise { diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts index f7ed66dada..6036188ee7 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts @@ -31,7 +31,7 @@ export class PublishDatabaseDialog { private sqlCmdVariablesTable: azdata.TableComponent | undefined; private formBuilder: azdata.FormBuilder | undefined; - private connection: azdata.connection.Connection | undefined; + private connectionId: string | undefined; private connectionIsDataSource: boolean | undefined; private profileSqlCmdVars: Record | undefined; @@ -155,20 +155,7 @@ export class PublishDatabaseDialog { if (this.connectionIsDataSource) { 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: [] - }; + const connProfile: azdata.IConnectionProfile = dataSource.getConnectionProfile(); if (dataSource.integratedSecurity) { connId = (await azdata.connection.connect(connProfile, false, false)).connectionId; @@ -178,11 +165,11 @@ export class PublishDatabaseDialog { } } else { - if (!this.connection) { + if (!this.connectionId) { throw new Error('Connection not defined.'); } - connId = this.connection?.connectionId; + connId = this.connectionId; } return await azdata.connection.getUriForConnection(connId); @@ -359,18 +346,19 @@ export class PublishDatabaseDialog { }).component(); editConnectionButton.onDidClick(async () => { - this.connection = await azdata.connection.openConnectionDialog(); + let connection = await azdata.connection.openConnectionDialog(); + this.connectionId = connection.connectionId; // show connection name if there is one, otherwise show connection string - if (this.connection.options['connectionName']) { - this.targetConnectionTextBox!.value = this.connection.options['connectionName']; + if (connection.options['connectionName']) { + this.targetConnectionTextBox!.value = connection.options['connectionName']; } else { - this.targetConnectionTextBox!.value = await azdata.connection.getConnectionString(this.connection.connectionId, false); + this.targetConnectionTextBox!.value = await azdata.connection.getConnectionString(connection.connectionId, false); } // change the database inputbox value to the connection's database if there is one - if (this.connection.options.database && this.connection.options.database !== constants.master) { - this.targetDatabaseTextBox!.value = this.connection.options.database; + if (connection.options.database && connection.options.database !== constants.master) { + this.targetDatabaseTextBox!.value = connection.options.database; } }); @@ -419,6 +407,10 @@ export class PublishDatabaseDialog { if (this.readPublishProfile) { const result = await this.readPublishProfile(fileUris[0]); (this.targetDatabaseTextBox).value = result.databaseName; + + this.connectionId = result.connectionId; + (this.targetConnectionTextBox).value = result.connectionString; + this.profileSqlCmdVars = result.sqlCmdVariables; const data = this.convertSqlCmdVarsToTableFormat(this.getSqlCmdVariablesForPublish()); diff --git a/extensions/sql-database-projects/src/models/IPublishSettings.ts b/extensions/sql-database-projects/src/models/IPublishSettings.ts index 4e80761b72..a5b90f0e4b 100644 --- a/extensions/sql-database-projects/src/models/IPublishSettings.ts +++ b/extensions/sql-database-projects/src/models/IPublishSettings.ts @@ -15,9 +15,3 @@ export interface IGenerateScriptSettings { connectionUri: string; sqlCmdVariables?: Record; } - -// only reading db name and SQLCMD vars from profile for now -export interface PublishProfile { - databaseName: string; - sqlCmdVariables: Record; -} diff --git a/extensions/sql-database-projects/src/models/dataSources/sqlConnectionStringSource.ts b/extensions/sql-database-projects/src/models/dataSources/sqlConnectionStringSource.ts index aa1ce86df3..669da9cd36 100644 --- a/extensions/sql-database-projects/src/models/dataSources/sqlConnectionStringSource.ts +++ b/extensions/sql-database-projects/src/models/dataSources/sqlConnectionStringSource.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as azdata from 'azdata'; import { DataSource } from './dataSources'; import * as constants from '../../common/constants'; @@ -70,6 +71,24 @@ export class SqlConnectionDataSource extends DataSource { public static fromJson(json: DataSourceJson): SqlConnectionDataSource { return new SqlConnectionDataSource(json.name, (json.data as unknown as SqlConnectionDataSourceJson).connectionString); } + + public getConnectionProfile(): azdata.IConnectionProfile { + const connProfile: azdata.IConnectionProfile = { + serverName: this.server, + databaseName: this.database, + connectionName: this.name, + userName: this.username, + password: this.password, + authenticationType: this.integratedSecurity ? 'Integrated' : 'SqlAuth', + savePassword: false, + providerName: 'MSSQL', + saveProfile: true, + id: this.name + '-dataSource', + options: [] + }; + + return connProfile; + } } /** diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index ccc533d645..6cee34d0e2 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -13,6 +13,7 @@ import * as os from 'os'; import { Uri } from 'vscode'; import { promises as fs } from 'fs'; import { DataSource } from './dataSources/dataSources'; +import { readSqlCmdVariables } from './publishProfile/publishProfile'; /** * Class representing a Project, and providing functions for operating on it @@ -80,7 +81,7 @@ export class Project { } // find all SQLCMD variables to include - this.sqlCmdVariables = utils.readSqlCmdVariables(this.projFileXmlDoc); + this.sqlCmdVariables = readSqlCmdVariables(this.projFileXmlDoc); // find all database references to include const references = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ArtifactReference); diff --git a/extensions/sql-database-projects/src/models/publishProfile/publishProfile.ts b/extensions/sql-database-projects/src/models/publishProfile/publishProfile.ts new file mode 100644 index 0000000000..3120af8f1b --- /dev/null +++ b/extensions/sql-database-projects/src/models/publishProfile/publishProfile.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as xmldom from 'xmldom'; +import * as constants from '../../common/constants'; +import * as utils from '../../common/utils'; + +import { promises as fs } from 'fs'; +import { Uri } from 'vscode'; +import { SqlConnectionDataSource } from '../dataSources/sqlConnectionStringSource'; + +// only reading db name, connection string, and SQLCMD vars from profile for now +export interface PublishProfile { + databaseName: string; + connectionId: string; + connectionString: string; + sqlCmdVariables: Record; +} + +/** + * parses the specified file to load publish settings + */ +export async function load(profileUri: Uri): Promise { + const profileText = await fs.readFile(profileUri.fsPath); + const profileXmlDoc = new xmldom.DOMParser().parseFromString(profileText.toString()); + + // read target database name + let targetDbName: string = ''; + let targetDatabaseNameCount = profileXmlDoc.documentElement.getElementsByTagName(constants.targetDatabaseName).length; + if (targetDatabaseNameCount > 0) { + // if there is more than one TargetDatabaseName nodes, SSDT uses the name in the last one so we'll do the same here + targetDbName = profileXmlDoc.documentElement.getElementsByTagName(constants.targetDatabaseName)[targetDatabaseNameCount - 1].textContent; + } + + const connectionInfo = await readConnectionString(profileXmlDoc); + + // get all SQLCMD variables to include from the profile + const sqlCmdVariables = readSqlCmdVariables(profileXmlDoc); + + return { + databaseName: targetDbName, + connectionId: connectionInfo.connectionId, + connectionString: connectionInfo.connectionString, + sqlCmdVariables: sqlCmdVariables + }; +} + +/** + * Read SQLCMD variables from xmlDoc and return them + * @param xmlDoc xml doc to read SQLCMD variables from. Format must be the same that sqlproj and publish profiles use + */ +export function readSqlCmdVariables(xmlDoc: any): Record { + let sqlCmdVariables: Record = {}; + for (let i = 0; i < xmlDoc.documentElement.getElementsByTagName(constants.SqlCmdVariable).length; i++) { + const sqlCmdVar = xmlDoc.documentElement.getElementsByTagName(constants.SqlCmdVariable)[i]; + const varName = sqlCmdVar.getAttribute(constants.Include); + + const varValue = sqlCmdVar.getElementsByTagName(constants.DefaultValue)[0].childNodes[0].nodeValue; + sqlCmdVariables[varName] = varValue; + } + + return sqlCmdVariables; +} + +async function readConnectionString(xmlDoc: any): Promise<{ connectionId: string, connectionString: string }> { + let targetConnectionString: string = ''; + let connId: string = ''; + + if (xmlDoc.documentElement.getElementsByTagName('TargetConnectionString').length > 0) { + targetConnectionString = xmlDoc.documentElement.getElementsByTagName('TargetConnectionString')[0].textContent; + const dataSource = new SqlConnectionDataSource('temp', targetConnectionString); + const connectionProfile = dataSource.getConnectionProfile(); + + try { + if (dataSource.integratedSecurity) { + connId = (await azdata.connection.connect(connectionProfile, false, false)).connectionId; + } + else { + connId = (await azdata.connection.openConnectionDialog(undefined, connectionProfile)).connectionId; + } + } catch (err) { + throw new Error(constants.unableToCreatePublishConnection(utils.getErrorMessage(err))); + } + } + + // mask password in connection string + targetConnectionString = await azdata.connection.getConnectionString(connId, false); + + return { + connectionId: connId, + connectionString: targetConnectionString + }; +} diff --git a/extensions/sql-database-projects/src/test/baselines/baselines.ts b/extensions/sql-database-projects/src/test/baselines/baselines.ts index 47de66866c..f390168888 100644 --- a/extensions/sql-database-projects/src/test/baselines/baselines.ts +++ b/extensions/sql-database-projects/src/test/baselines/baselines.ts @@ -16,7 +16,8 @@ export let SSDTUpdatedProjectBaseline: string; export let SSDTUpdatedProjectAfterSystemDbUpdateBaseline: string; export let SSDTProjectBaselineWithCleanTarget: string; export let SSDTProjectBaselineWithCleanTargetAfterUpdate: string; -export let publishProfileBaseline: string; +export let publishProfileIntegratedSecurityBaseline: string; +export let publishProfileSqlLoginBaseline: string; const baselineFolderPath = __dirname; @@ -30,7 +31,8 @@ export async function loadBaselines() { SSDTUpdatedProjectAfterSystemDbUpdateBaseline = await loadBaseline(baselineFolderPath, 'SSDTUpdatedProjectAfterSystemDbUpdateBaseline.xml'); SSDTProjectBaselineWithCleanTarget = await loadBaseline(baselineFolderPath, 'SSDTProjectBaselineWithCleanTarget.xml'); SSDTProjectBaselineWithCleanTargetAfterUpdate = await loadBaseline(baselineFolderPath, 'SSDTProjectBaselineWithCleanTargetAfterUpdate.xml'); - publishProfileBaseline = await loadBaseline(baselineFolderPath, 'publishProfileBaseline.publish.xml'); + publishProfileIntegratedSecurityBaseline = await loadBaseline(baselineFolderPath, 'publishProfileIntegratedSecurityBaseline.publish.xml'); + publishProfileSqlLoginBaseline = await loadBaseline(baselineFolderPath, 'publishProfileSqlLoginBaseline.publish.xml'); } async function loadBaseline(baselineFolderPath: string, fileName: string): Promise { diff --git a/extensions/sql-database-projects/src/test/baselines/publishProfileBaseline.publish.xml b/extensions/sql-database-projects/src/test/baselines/publishProfileIntegratedSecurityBaseline.publish.xml similarity index 71% rename from extensions/sql-database-projects/src/test/baselines/publishProfileBaseline.publish.xml rename to extensions/sql-database-projects/src/test/baselines/publishProfileIntegratedSecurityBaseline.publish.xml index 1c6750dcc2..e44e75f607 100644 --- a/extensions/sql-database-projects/src/test/baselines/publishProfileBaseline.publish.xml +++ b/extensions/sql-database-projects/src/test/baselines/publishProfileIntegratedSecurityBaseline.publish.xml @@ -4,6 +4,7 @@ True targetDb DatabaseProject1.sql + Data Source=testserver;Integrated Security=true;Persist Security Info=False;Pooling=False;MultipleActiveResultSets=False;Connect Timeout=60;Encrypt=False;TrustServerCertificate=True 1 diff --git a/extensions/sql-database-projects/src/test/baselines/publishProfileSqlLoginBaseline.publish.xml b/extensions/sql-database-projects/src/test/baselines/publishProfileSqlLoginBaseline.publish.xml new file mode 100644 index 0000000000..92b2c814ab --- /dev/null +++ b/extensions/sql-database-projects/src/test/baselines/publishProfileSqlLoginBaseline.publish.xml @@ -0,0 +1,16 @@ + + + + True + targetDb + DatabaseProject1.sql + Data Source=testserver;User Id=testUser;Password=abcd123;Integrated Security=false;Persist Security Info=False;Pooling=False;MultipleActiveResultSets=False;Connect Timeout=60;Encrypt=False;TrustServerCertificate=True + 1 + + + + MyProdDatabase + $(SqlCmdVar__1) + + + diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index 270682a7a6..708759e5a0 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -267,10 +267,12 @@ describe ('ProjectsController', function(): void { holler = publishHoller; return Promise.resolve(undefined); }); - projController.setup(x => x.readPublishProfile(TypeMoq.It.isAny())).returns(() => { + projController.setup(x => x.readPublishProfileCallback(TypeMoq.It.isAny())).returns(() => { holler = profileHoller; return Promise.resolve({ databaseName: '', + connectionId: '', + connectionString: '', sqlCmdVariables: {} }); }); @@ -291,21 +293,11 @@ describe ('ProjectsController', function(): void { 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')); + await projController.object.readPublishProfileCallback(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'; diff --git a/extensions/sql-database-projects/src/test/publishProfile.test.ts b/extensions/sql-database-projects/src/test/publishProfile.test.ts new file mode 100644 index 0000000000..c189940b42 --- /dev/null +++ b/extensions/sql-database-projects/src/test/publishProfile.test.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as should from 'should'; +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import * as sinon from 'sinon'; +import * as baselines from './baselines/baselines'; +import * as testUtils from './testUtils'; +import * as constants from '../common/constants'; +import { ProjectsController } from '../controllers/projectController'; +import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProjectTreeViewProvider'; + + +describe('Publish profile tests', function (): void { + before(async function (): Promise { + await baselines.loadBaselines(); + }); + + afterEach(function (): void { + sinon.restore(); + }); + + it('Should read database name, integrated security connection string, and SQLCMD variables from publish profile', async function (): Promise { + await baselines.loadBaselines(); + let profilePath = await testUtils.createTestFile(baselines.publishProfileIntegratedSecurityBaseline, 'publishProfile.publish.xml'); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + const connectionResult = { + connected: true, + connectionId: 'connId', + errorMessage: '', + errorCode: 0 + }; + const connectionString = 'Data Source=testserver;Integrated Security=true;Persist Security Info=False;Pooling=False;MultipleActiveResultSets=False;Connect Timeout=60;Encrypt=False;TrustServerCertificate=True'; + sinon.stub(azdata.connection, 'connect').resolves(connectionResult); + sinon.stub(azdata.connection, 'getConnectionString').resolves(connectionString); + + let result = await projController.readPublishProfileCallback(vscode.Uri.file(profilePath)); + should(result.databaseName).equal('targetDb'); + should(Object.keys(result.sqlCmdVariables).length).equal(1); + should(result.sqlCmdVariables['ProdDatabaseName']).equal('MyProdDatabase'); + should(result.connectionId).equal('connId'); + should(result.connectionString).equal('Data Source=testserver;Integrated Security=true;Persist Security Info=False;Pooling=False;MultipleActiveResultSets=False;Connect Timeout=60;Encrypt=False;TrustServerCertificate=True'); + }); + + it('Should read database name, SQL login connection string, and SQLCMD variables from publish profile', async function (): Promise { + await baselines.loadBaselines(); + let profilePath = await testUtils.createTestFile(baselines.publishProfileSqlLoginBaseline, 'publishProfile.publish.xml'); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + const connectionResult = { + providerName: 'MSSQL', + connectionId: 'connId', + options: {} + }; + const connectionString = 'Data Source=testserver;User Id=testUser;Password=******;Integrated Security=false;Persist Security Info=False;Pooling=False;MultipleActiveResultSets=False;Connect Timeout=60;Encrypt=False;TrustServerCertificate=True'; + sinon.stub(azdata.connection, 'openConnectionDialog').resolves(connectionResult); + sinon.stub(azdata.connection, 'getConnectionString').resolves(connectionString); + + let result = await projController.readPublishProfileCallback(vscode.Uri.file(profilePath)); + should(result.databaseName).equal('targetDb'); + should(Object.keys(result.sqlCmdVariables).length).equal(1); + should(result.sqlCmdVariables['ProdDatabaseName']).equal('MyProdDatabase'); + should(result.connectionId).equal('connId'); + should(result.connectionString).equal('Data Source=testserver;User Id=testUser;Password=******;Integrated Security=false;Persist Security Info=False;Pooling=False;MultipleActiveResultSets=False;Connect Timeout=60;Encrypt=False;TrustServerCertificate=True'); + }); + + it('Should throw error when connecting does not work', async function (): Promise { + await baselines.loadBaselines(); + let profilePath = await testUtils.createTestFile(baselines.publishProfileIntegratedSecurityBaseline, 'publishProfile.publish.xml'); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + + const connectionString = 'Data Source=testserver;Integrated Security=true;Persist Security Info=False;Pooling=False;MultipleActiveResultSets=False;Connect Timeout=60;Encrypt=False;TrustServerCertificate=True'; + sinon.stub(azdata.connection, 'connect').throws(new Error('Could not connect')); + sinon.stub(azdata.connection, 'getConnectionString').resolves(connectionString); + + await testUtils.shouldThrowSpecificError(async () => await projController.readPublishProfileCallback(vscode.Uri.file(profilePath)), constants.unableToCreatePublishConnection('Could not connect')); + }); +});