Skip to content

Commit

Permalink
sqlpackage path argument, dotnet tool sqlpackage (#221)
Browse files Browse the repository at this point in the history
  • Loading branch information
dzsquared authored Jun 28, 2024
1 parent 1107fd5 commit 83d6b65
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 66 deletions.
4 changes: 2 additions & 2 deletions __tests__/AzureSqlAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Constants from '../src/Constants';

describe('AzureSqlAction tests', () => {
afterEach(() => {
jest.restoreAllMocks();
jest.restoreAllMocks();
});

describe('validate sqlpackage calls for DacpacAction', () => {
Expand Down Expand Up @@ -216,7 +216,7 @@ describe('validate connection string escaping in sqlpackage commands', () => {
* @param connectionString The custom connection string to be used for the test. If not specified, a default one using SQL login will be used.
* @returns An ActionInputs objects based on the given action type.
*/
function getInputs(actionType: ActionType, connectionString: string = ''): IActionInputs {
export function getInputs(actionType: ActionType, connectionString: string = ''): IActionInputs {

const defaultConnectionString = 'Server=testServer.database.windows.net;Initial Catalog=testDB;User Id=testUser;Password=placeholder';
const config = connectionString ? new SqlConnectionConfig(connectionString) : new SqlConnectionConfig(defaultConnectionString);
Expand Down
78 changes: 78 additions & 0 deletions __tests__/AzureSqlActionHelper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as fs from 'fs';
import * as semver from 'semver';
import AzureSqlActionHelper from "../src/AzureSqlActionHelper";
import AzureSqlAction, { IBuildAndPublishInputs, IDacpacActionInputs, ActionType, SqlPackageAction, IActionInputs } from "../src/AzureSqlAction";
import { getInputs } from './AzureSqlAction.test';

jest.mock('fs', () => {
return {
__esModule: true,
...jest.requireActual('fs')
};
});

describe('AzureSqlActionHelper tests', () => {
afterEach(() => {
jest.restoreAllMocks();
});

const versions = [ // returned from sqlpackage, validated version expected
['162.3.563.1', '162.3.563'], // GA version
['162.4.101.0', '162.4.101'], // GA version
['162.3.562-preview', '162.3.562'], // preview version
['15.0.5164.1', '15.0.5164'] // old version
];

// checks parsing logic used from semver
describe('tests semver parsing for potential sqlpackage version values', () => {
it.each(versions)('should parse %s version correctly', (versionReturned, versionExpected) => {
let semverExpected = semver.coerce(versionExpected);
let semverTested = semver.coerce(versionReturned);

expect(semverTested).toEqual(semverExpected);
});
});

// checks sorting logic used from semver
describe('tests sorting of versions to select latest version', () => {
it('should select latest version', () => {
let versionArray: semver.SemVer[] = [];
versions.forEach(([versionReturned, versionExpected]) => {
versionArray.push(semver.coerce(versionReturned) ?? new semver.SemVer('0.0.0'));
});

let latestVersion = semver.rsort(versionArray)[0];
let latestVersionExpected = semver.coerce(versions[1][1]);

expect(latestVersion).toEqual(latestVersionExpected);
});
});


// ensures the sqlpackagepath input overrides the version check
describe('sqlpackagepath input options', () => {
const sqlpackagepaths = ['//custom/path/to/sqlpackage', 'c:/Program Files/Sqlpackage/sqlpackage'];
it.each(sqlpackagepaths)('should return sqlpackagepath if provided', async (path) => {
let inputs = getInputs(ActionType.DacpacAction) as IDacpacActionInputs;
inputs.sqlpackagePath = path;

let fileExistsSpy = jest.spyOn(fs, 'existsSync').mockReturnValue(true);
let sqlpackagePath = await AzureSqlActionHelper.getSqlPackagePath(inputs);

expect(fileExistsSpy).toHaveBeenCalledWith(inputs.sqlpackagePath);
expect(sqlpackagePath).toEqual(path);
});
});

it('throws if SqlPackage.exe fails to be found at user-specified location', async () => {
let inputs = getInputs(ActionType.DacpacAction) as IDacpacActionInputs;
let action = new AzureSqlAction(inputs);

let getSqlPackagePathSpy = jest.spyOn(AzureSqlActionHelper, 'getSqlPackagePath').mockRejectedValue(1);

expect(await action.execute().catch(() => null)).rejects;
expect(getSqlPackagePathSpy).toHaveBeenCalledTimes(1);
});

});

2 changes: 1 addition & 1 deletion __tests__/FirewallManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('FirewallManager tests', () => {
});
});

it('does not add firewall rule if client has access to MySql server', async () => {
it('does not add firewall rule if client has access to SQL server', async () => {
let addFirewallRuleSpy = jest.spyOn(azureSqlResourceManager, 'addFirewallRule').mockResolvedValue({ name: 'FirewallRuleName' } as any);
let removeFirewallRuleSpy = jest.spyOn(azureSqlResourceManager, 'removeFirewallRule');
await firewallManager.addFirewallRule('');
Expand Down
52 changes: 49 additions & 3 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('main.ts tests', () => {

expect(detectIPAddressSpy).toHaveBeenCalled();
expect(getAuthorizerSpy).not.toHaveBeenCalled();
expect(getInputSpy).toHaveBeenCalledTimes(5);
expect(getInputSpy).toHaveBeenCalledTimes(6);
expect(resolveFilePathSpy).toHaveBeenCalled();
expect(addFirewallRuleSpy).not.toHaveBeenCalled();
expect(actionExecuteSpy).toHaveBeenCalled();
Expand Down Expand Up @@ -101,14 +101,60 @@ describe('main.ts tests', () => {

expect(detectIPAddressSpy).toHaveBeenCalled();
expect(getAuthorizerSpy).not.toHaveBeenCalled();
expect(getInputSpy).toHaveBeenCalledTimes(4);
expect(getInputSpy).toHaveBeenCalledTimes(5);
expect(resolveFilePathSpy).toHaveBeenCalled();
expect(addFirewallRuleSpy).not.toHaveBeenCalled();
expect(actionExecuteSpy).toHaveBeenCalled();
expect(removeFirewallRuleSpy).not.toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
})

it('gets inputs and executes dacpac action with optional sqlpackage path', async () => {
let resolveFilePathSpy = jest.spyOn(AzureSqlActionHelper, 'resolveFilePath').mockReturnValue('./TestDacpacPackage.dacpac');
let getInputSpy = jest.spyOn(core, 'getInput').mockImplementation((name, options) => {
switch(name) {
case 'connection-string': return 'Server=testServer.database.windows.net;Initial Catalog=testDB;User Id=testUser;Password=placeholder;';
case 'path': return './TestDacpacPackage.dacpac';
case 'action': return 'script';
case 'arguments': return '/p:FakeSqlpackageArgument';
case 'sqlpackage-path': return '/path/to/sqlpackage';
}

return '';
});

let getAuthorizerSpy = jest.spyOn(AuthorizerFactory, 'getAuthorizer');
let addFirewallRuleSpy = jest.spyOn(FirewallManager.prototype, 'addFirewallRule');
let actionExecuteSpy = jest.spyOn(AzureSqlAction.prototype, 'execute');
let removeFirewallRuleSpy = jest.spyOn(FirewallManager.prototype, 'removeFirewallRule');
let setFailedSpy = jest.spyOn(core, 'setFailed');
let detectIPAddressSpy = SqlUtils.detectIPAddress = jest.fn().mockImplementationOnce(() => {
return "";
});

await run();

expect(AzureSqlAction).toHaveBeenCalled();
expect(AzureSqlAction).toHaveBeenCalledWith(
expect.objectContaining({
actionType: ActionType.DacpacAction,
filePath: './TestDacpacPackage.dacpac',
sqlpackageAction: SqlPackageAction.Script,
additionalArguments: '/p:FakeSqlpackageArgument',
sqlpackagePath: '/path/to/sqlpackage'
} as IDacpacActionInputs)
);

expect(detectIPAddressSpy).toHaveBeenCalled();
expect(getAuthorizerSpy).not.toHaveBeenCalled();
expect(getInputSpy).toHaveBeenCalledTimes(5);
expect(resolveFilePathSpy).toHaveBeenCalled();
expect(addFirewallRuleSpy).not.toHaveBeenCalled();
expect(actionExecuteSpy).toHaveBeenCalled();
expect(removeFirewallRuleSpy).not.toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
})

it('gets inputs and executes sql action', async () => {
let resolveFilePathSpy = jest.spyOn(AzureSqlActionHelper, 'resolveFilePath').mockReturnValue('./TestSqlFile.sql');
let getInputSpy = jest.spyOn(core, 'getInput').mockImplementation((name, options) => {
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ inputs:
arguments:
description: 'In case of .dacpac or .sqlproj file types, additional sqlpackage arguments that will be applied. In case of .sql file type, additional go-sqlcmd argument that will be applied.'
required: false
sqlpackage-path:
description: 'Specify a SqlPackage executable location to override the default locations.'
required: false
build-arguments:
description: 'In case of a .sqlproj file, additional arguments that will be applied to dotnet build when building the database project.'
required: false
Expand Down
2 changes: 1 addition & 1 deletion lib/main.js

Large diffs are not rendered by default.

32 changes: 23 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@
"@tediousjs/connection-string": "^0.5.0",
"azure-actions-webclient": "^1.0.3",
"glob": "^10.3.12",
"semver": "^7.6.2",
"uuid": "^3.3.2",
"winreg": "^1.2.4"
},
"devDependencies": {
"@types/glob": "^8.1.0",
"@types/jest": "^24.0.13",
"@types/node": "^12.0.4",
"@types/semver": "^7.5.8",
"@types/uuid": "^3.4.4",
"@types/winreg": "^1.2.30",
"jest": "^29.7.0",
Expand Down
9 changes: 5 additions & 4 deletions src/AzureSqlAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export interface IActionInputs {

export interface IDacpacActionInputs extends IActionInputs {
sqlpackageAction: SqlPackageAction;
sqlpackagePath?: string;
}

export interface IBuildAndPublishInputs extends IActionInputs {
sqlpackageAction: SqlPackageAction;
export interface IBuildAndPublishInputs extends IDacpacActionInputs {
buildArguments?: string;
}

Expand Down Expand Up @@ -63,7 +63,8 @@ export default class AzureSqlAction {
connectionConfig: buildAndPublishInputs.connectionConfig,
filePath: dacpacPath,
additionalArguments: buildAndPublishInputs.additionalArguments,
sqlpackageAction: buildAndPublishInputs.sqlpackageAction
sqlpackageAction: buildAndPublishInputs.sqlpackageAction,
sqlpackagePath: buildAndPublishInputs.sqlpackagePath
} as IDacpacActionInputs;
await this._executeDacpacAction(publishInputs);
}
Expand All @@ -74,7 +75,7 @@ export default class AzureSqlAction {

private async _executeDacpacAction(inputs: IDacpacActionInputs) {
core.debug('Begin executing sqlpackage');
let sqlPackagePath = await AzureSqlActionHelper.getSqlPackagePath();
let sqlPackagePath = await AzureSqlActionHelper.getSqlPackagePath(inputs);
let sqlPackageArgs = this._getSqlPackageArguments(inputs);

await exec.exec(`"${sqlPackagePath}" ${sqlPackageArgs}`);
Expand Down
Loading

0 comments on commit 83d6b65

Please sign in to comment.