From b460b7834c88e322f92c1621b64c552aa1d6d0d7 Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Thu, 15 Oct 2020 10:48:09 -0700 Subject: [PATCH] Add more azdata tests (#12902) * Add more azdata tests * fix build * comments --- extensions/azdata/src/azdata.ts | 26 +-- extensions/azdata/src/azdataReleaseInfo.ts | 2 +- ...rcControllerConfigProfilesOptionsSource.ts | 3 +- extensions/azdata/src/test/azdata.test.ts | 180 +++++++++++++++++- .../azdata/src/test/azdataReleaseInfo.test.ts | 75 ++++++++ ...trollerConfigProfilesOptionsSource.test.ts | 38 ++++ .../test/services/azdataToolService.test.ts | 17 ++ 7 files changed, 324 insertions(+), 17 deletions(-) create mode 100644 extensions/azdata/src/test/azdataReleaseInfo.test.ts create mode 100644 extensions/azdata/src/test/providers/arcControllerConfigProfilesOptionsSource.test.ts create mode 100644 extensions/azdata/src/test/services/azdataToolService.test.ts diff --git a/extensions/azdata/src/azdata.ts b/extensions/azdata/src/azdata.ts index 4ea09ffae7..9477057f87 100644 --- a/extensions/azdata/src/azdata.ts +++ b/extensions/azdata/src/azdata.ts @@ -62,7 +62,7 @@ export class AzdataTool implements azdataExt.IAzdataApi { public arc = { dc: { - create: async (namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string): Promise> => { + create: (namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string): Promise> => { const args = ['arc', 'dc', 'create', '--namespace', namespace, '--name', name, @@ -79,31 +79,31 @@ export class AzdataTool implements azdataExt.IAzdataApi { return this.executeCommand(args); }, endpoint: { - list: async () => { + list: (): Promise> => { return this.executeCommand(['arc', 'dc', 'endpoint', 'list']); } }, config: { - list: async () => { + list: (): Promise> => { return this.executeCommand(['arc', 'dc', 'config', 'list']); }, - show: async () => { + show: (): Promise> => { return this.executeCommand(['arc', 'dc', 'config', 'show']); } } }, postgres: { server: { - delete: async (name: string) => { + delete: (name: string): Promise> => { return this.executeCommand(['arc', 'postgres', 'server', 'delete', '-n', name]); }, - list: async () => { + list: (): Promise> => { return this.executeCommand(['arc', 'postgres', 'server', 'list']); }, - show: async (name: string) => { + show: (name: string): Promise> => { return this.executeCommand(['arc', 'postgres', 'server', 'show', '-n', name]); }, - edit: async ( + edit: ( name: string, args: { adminPassword?: boolean, @@ -118,7 +118,7 @@ export class AzdataTool implements azdataExt.IAzdataApi { replaceEngineSettings?: boolean, workers?: number }, - additionalEnvVars?: { [key: string]: string }) => { + additionalEnvVars?: { [key: string]: string }): Promise> => { const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name]; if (args.adminPassword) { argsArray.push('--admin-password'); } if (args.coresLimit !== undefined) { argsArray.push('--cores-limit', args.coresLimit); } @@ -137,20 +137,20 @@ export class AzdataTool implements azdataExt.IAzdataApi { }, sql: { mi: { - delete: async (name: string) => { + delete: (name: string): Promise> => { return this.executeCommand(['arc', 'sql', 'mi', 'delete', '-n', name]); }, - list: async () => { + list: (): Promise> => { return this.executeCommand(['arc', 'sql', 'mi', 'list']); }, - show: async (name: string) => { + show: (name: string): Promise> => { return this.executeCommand(['arc', 'sql', 'mi', 'show', '-n', name]); } } } }; - public async login(endpoint: string, username: string, password: string): Promise> { + public login(endpoint: string, username: string, password: string): Promise> { return this.executeCommand(['login', '-e', endpoint, '-u', username], { 'AZDATA_PASSWORD': password }); } diff --git a/extensions/azdata/src/azdataReleaseInfo.ts b/extensions/azdata/src/azdataReleaseInfo.ts index adac99ad7d..d31682fa0e 100644 --- a/extensions/azdata/src/azdataReleaseInfo.ts +++ b/extensions/azdata/src/azdataReleaseInfo.ts @@ -16,7 +16,7 @@ interface PlatformReleaseInfo { link?: string; // "https://aka.ms/azdata-msi" } -interface AzdataReleaseInfo { +export interface AzdataReleaseInfo { win32: PlatformReleaseInfo, darwin: PlatformReleaseInfo, linux: PlatformReleaseInfo diff --git a/extensions/azdata/src/providers/arcControllerConfigProfilesOptionsSource.ts b/extensions/azdata/src/providers/arcControllerConfigProfilesOptionsSource.ts index 3108afe80c..d90ecd915d 100644 --- a/extensions/azdata/src/providers/arcControllerConfigProfilesOptionsSource.ts +++ b/extensions/azdata/src/providers/arcControllerConfigProfilesOptionsSource.ts @@ -13,7 +13,8 @@ export class ArcControllerConfigProfilesOptionsSource implements rd.IOptionsSour readonly optionsSourceId = 'arc.controller.config.profiles'; constructor(private _azdataExtApi: azdataExt.IExtension) { } async getOptions(): Promise { - if (!this._azdataExtApi.isEulaAccepted()) { // if eula has not yet be accepted then give user a chance to accept it + const isEulaAccepted = await this._azdataExtApi.isEulaAccepted(); + if (!isEulaAccepted) { // if eula has not yet be accepted then give user a chance to accept it await this._azdataExtApi.promptForEula(); } return (await this._azdataExtApi.azdata.arc.dc.config.list()).result; diff --git a/extensions/azdata/src/test/azdata.test.ts b/extensions/azdata/src/test/azdata.test.ts index e1725c82b1..e3b9950556 100644 --- a/extensions/azdata/src/test/azdata.test.ts +++ b/extensions/azdata/src/test/azdata.test.ts @@ -11,6 +11,9 @@ import * as childProcess from '../common/childProcess'; import { HttpClient } from '../common/httpClient'; import * as utils from '../common/utils'; import * as loc from '../localizedConstants'; +import * as os from 'os'; +import * as fs from 'fs'; +import { AzdataReleaseInfo } from '../azdataReleaseInfo'; const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', '0.0.0'); @@ -18,7 +21,7 @@ const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', '0.0.0'); * This matches the schema of the JSON file used to determine the current version of * azdata - do not modify unless also updating the corresponding JSON file */ -const releaseJson = { +const releaseJson: AzdataReleaseInfo = { win32: { 'version': '9999.999.999', 'link': 'https://download.com/azdata-20.0.1.msi' @@ -36,8 +39,181 @@ describe('azdata', function () { afterEach(function (): void { sinon.restore(); }); + describe('azdataTool', function (): void { + const azdataTool = new azdata.AzdataTool(os.tmpdir(), '1.0.0'); + let executeCommandStub: sinon.SinonStub; + const namespace = 'myNamespace'; + const name = 'myName'; + const connectivityMode = 'myConnectivityMode'; + const resourceGroup = 'myResourceGroup'; + const location = 'myLocation'; + const subscription = 'mySubscription'; + const profileName = 'myProfileName'; + const storageClass = 'myStorageClass'; - describe('findAzdata', function () { + beforeEach(function (): void { + executeCommandStub = sinon.stub(childProcess, 'executeCommand').resolves({ stdout: '{}', stderr: '' }); + }); + + describe('arc', function (): void { + describe('dc', function (): void { + it('create', async function (): Promise { + await azdataTool.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass); + verifyExecuteCommandCalledWithArgs([ + 'arc', 'dc', 'create', + namespace, + name, + connectivityMode, + resourceGroup, + location, + subscription, + profileName, + storageClass]); + }); + describe('endpoint', async function (): Promise { + it('list', async function (): Promise { + await azdataTool.arc.dc.endpoint.list(); + verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'endpoint', 'list']); + }); + }); + describe('config', async function (): Promise { + it('list', async function (): Promise { + await azdataTool.arc.dc.config.list(); + verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'list']); + }); + it('show', async function (): Promise { + await azdataTool.arc.dc.config.show(); + verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show']); + }); + }); + }); + describe('postgres', function (): void { + describe('server', function (): void { + it('delete', async function (): Promise { + await azdataTool.arc.postgres.server.delete(name); + verifyExecuteCommandCalledWithArgs(['arc', 'postgres', 'server', 'delete', name]); + }); + it('list', async function (): Promise { + await azdataTool.arc.postgres.server.list(); + verifyExecuteCommandCalledWithArgs(['arc', 'postgres', 'server', 'list']); + }); + it('show', async function (): Promise { + await azdataTool.arc.postgres.server.show(name); + verifyExecuteCommandCalledWithArgs(['arc', 'postgres', 'server', 'show', name]); + }); + it('edit', async function (): Promise { + const args = { + adminPassword: true, + coresLimit: 'myCoresLimit', + coresRequest: 'myCoresRequest', + engineSettings: 'myEngineSettings', + extensions: 'myExtensions', + memoryLimit: 'myMemoryLimit', + memoryRequest: 'myMemoryRequest', + noWait: true, + port: 1337, + replaceEngineSettings: true, + workers: 2 + }; + await azdataTool.arc.postgres.server.edit(name, args); + verifyExecuteCommandCalledWithArgs([ + 'arc', 'postgres', 'server', 'edit', + name, + '--admin-password', + args.coresLimit, + args.coresRequest, + args.engineSettings, + args.extensions, + args.memoryLimit, + args.memoryRequest, + '--no-wait', + args.port.toString(), + '--replace-engine-settings', + args.workers.toString()]); + }); + }); + }); + describe('sql', function (): void { + describe('mi', function (): void { + it('delete', async function (): Promise { + await azdataTool.arc.sql.mi.delete(name); + verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'delete', name]); + }); + it('list', async function (): Promise { + await azdataTool.arc.sql.mi.list(); + verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list']); + }); + it('show', async function (): Promise { + await azdataTool.arc.sql.mi.show(name); + verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'show', name]); + }); + }); + }); + it('login', async function (): Promise { + const endpoint = 'myEndpoint'; + const username = 'myUsername'; + const password = 'myPassword'; + await azdataTool.login(endpoint, username, password); + verifyExecuteCommandCalledWithArgs(['login', endpoint, username]); + }); + it('version', async function (): Promise { + executeCommandStub.resolves({ stdout: '1.0.0', stderr: '' }); + await azdataTool.version(); + verifyExecuteCommandCalledWithArgs(['--version']); + }); + it('general error throws', async function (): Promise { + const err = new Error(); + executeCommandStub.throws(err); + try { + await azdataTool.arc.dc.endpoint.list(); + throw new Error('command should have failed'); + } catch (error) { + should(error).equal(err); + } + }); + it('ExitCodeError handled and parsed correctly', async function (): Promise { + const errorInnerText = 'my error text'; + const err = new childProcess.ExitCodeError(1, `ERROR { "stderr": "${errorInnerText}"}`); + executeCommandStub.throws(err); + try { + await azdataTool.arc.dc.endpoint.list(); + throw new Error('command should have failed'); + } catch (error) { + should(error).equal(err); + should((error as childProcess.ExitCodeError).stderr).equal(errorInnerText); + } + }); + it('ExitCodeError general error with azdata tool existing rethrows original error', async function (): Promise { + sinon.stub(fs.promises, 'access').resolves(); + const err = new childProcess.ExitCodeError(1, 'some other error'); + executeCommandStub.throws(err); + try { + await azdataTool.arc.dc.endpoint.list(); + throw new Error('command should have failed'); + } catch (error) { + should(error).equal(err); + } + }); + it('ExitCodeError general error with azdata tool not existing throws NoAzdataError', async function (): Promise { + sinon.stub(fs.promises, 'access').throws(new Error('not found')); + const err = new childProcess.ExitCodeError(1, 'some other error'); + executeCommandStub.throws(err); + try { + await azdataTool.arc.dc.endpoint.list(); + throw new Error('command should have failed'); + } catch (error) { + should(error instanceof utils.NoAzdataError).be.true('error should have been instance of NoAzdataError'); + } + }); + }); + + function verifyExecuteCommandCalledWithArgs(args: string[]): void { + const commandArgs = executeCommandStub.args[0][1] as string[]; + args.forEach(arg => should(commandArgs).containEql(arg)); + } + }); + + describe('findAzdata', function (): void { it('successful', async function (): Promise { // Mock searchForCmd to return a path to azdata.cmd sinon.stub(utils, 'searchForCmd').returns(Promise.resolve('/path/to/azdata')); diff --git a/extensions/azdata/src/test/azdataReleaseInfo.test.ts b/extensions/azdata/src/test/azdataReleaseInfo.test.ts new file mode 100644 index 0000000000..cf0ce3eff6 --- /dev/null +++ b/extensions/azdata/src/test/azdataReleaseInfo.test.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sinon from 'sinon'; +import { HttpClient } from '../common/httpClient'; +import { getPlatformReleaseVersion, getPlatformDownloadLink, AzdataReleaseInfo } from '../azdataReleaseInfo'; + +const emptyReleaseJson = { + win32: {}, + darwin: {}, + linux: {} +}; + +const releaseVersion = '999.999.999'; +const releaseLink = 'https://microsoft.com'; + +const validReleaseJson: AzdataReleaseInfo = { + win32: { + version: releaseVersion, + link: releaseLink + }, + darwin: { + version: releaseVersion, + link: releaseLink + }, + linux: { + version: releaseVersion, + link: releaseLink + } +}; + +describe('azdataReleaseInfo', function (): void { + afterEach(function (): void { + sinon.restore(); + }); + + describe('getPlatformReleaseVersion', function(): void { + it('gets version successfully', async function(): Promise { + sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(validReleaseJson)); + const version = await getPlatformReleaseVersion(); + should(version.format()).equal(releaseVersion); + }); + + it('throws with invalid JSON', async function (): Promise { + sinon.stub(HttpClient, 'getTextContent').resolves('invalid JSON'); + await should(getPlatformReleaseVersion()).be.rejected(); + }); + + it('throws when no version', async function (): Promise { + sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(emptyReleaseJson)); + await should(getPlatformReleaseVersion()).be.rejected(); + }); + }); + + describe('getPlatformDownloadLink', function(): void { + it('gets link successfully', async function(): Promise { + sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(validReleaseJson)); + const link = await getPlatformDownloadLink(); + should(link).equal(releaseLink); + }); + + it('throws with invalid JSON', async function (): Promise { + sinon.stub(HttpClient, 'getTextContent').resolves('invalid JSON'); + await should(getPlatformDownloadLink()).be.rejected(); + }); + + it('throws when no version', async function (): Promise { + sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(emptyReleaseJson)); + await should(getPlatformDownloadLink()).be.rejected(); + }); + }); +}); diff --git a/extensions/azdata/src/test/providers/arcControllerConfigProfilesOptionsSource.test.ts b/extensions/azdata/src/test/providers/arcControllerConfigProfilesOptionsSource.test.ts new file mode 100644 index 0000000000..56996e4ac5 --- /dev/null +++ b/extensions/azdata/src/test/providers/arcControllerConfigProfilesOptionsSource.test.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * 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 azdataExt from 'azdata-ext'; +import * as should from 'should'; +import * as sinon from 'sinon'; +import { ArcControllerConfigProfilesOptionsSource } from '../../providers/arcControllerConfigProfilesOptionsSource'; + +describe('arcControllerConfigProfilesOptionsSource', async function (): Promise { + afterEach(function(): void { + sinon.restore(); + }); + + it('eula accepted returns list', async function (): Promise { + const options = ['option1', 'option2']; + const api = vscode.extensions.getExtension(azdataExt.extension.name)?.exports as azdataExt.IExtension; + sinon.stub(api, 'isEulaAccepted').resolves(true); + sinon.stub(api.azdata.arc.dc.config, 'list').resolves({ stdout: [''], stderr: [''], logs: [''], result: options}); + const source = new ArcControllerConfigProfilesOptionsSource(api); + const result = await source.getOptions(); + should(result).deepEqual(options); + }); + + it('eula not accepted prompts for acceptance', async function (): Promise { + const options = ['option1', 'option2']; + const api = vscode.extensions.getExtension(azdataExt.extension.name)?.exports as azdataExt.IExtension; + sinon.stub(api, 'isEulaAccepted').resolves(false); + const promptStub = sinon.stub(api, 'promptForEula').resolves(true); + sinon.stub(api.azdata.arc.dc.config, 'list').resolves({ stdout: [''], stderr: [''], logs: [''], result: options}); + const source = new ArcControllerConfigProfilesOptionsSource(api); + const result = await source.getOptions(); + should(result).deepEqual(options); + should(promptStub.calledOnce).be.true('promptForEula should have been called'); + }); +}); diff --git a/extensions/azdata/src/test/services/azdataToolService.test.ts b/extensions/azdata/src/test/services/azdataToolService.test.ts new file mode 100644 index 0000000000..dbd2402cce --- /dev/null +++ b/extensions/azdata/src/test/services/azdataToolService.test.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +import * as should from 'should'; +import { AzdataTool } from '../../azdata'; +import { AzdataToolService } from '../../services/azdataToolService'; + +describe('azdataToolService', function (): void { + it('Tool should be set correctly', async function (): Promise { + const service = new AzdataToolService(); + should(service.localAzdata).be.undefined(); + service.localAzdata = new AzdataTool('my path', '1.0.0'); + should(service).not.be.undefined(); + }); +});