From ce39b4bd19ca9bd755182ee274edf815de67177b Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Fri, 19 Mar 2021 13:12:26 -0700 Subject: [PATCH] Arc - Unit tests for deleting Postgres (#14502) * tests for deleting postgres from overview page * upgrade to azdata-test 1.5.0 * Fix api stubbing Co-authored-by: Brian Bergeron Co-authored-by: chgagnon --- extensions/arc/package.json | 2 +- .../arc/src/test/mocks/fakeAzdataApi.ts | 123 ++++++++++-------- .../arc/src/test/models/postgresModel.test.ts | 42 ++---- .../dashboards/postgresOverviewPage.test.ts | 110 ++++++++++++++++ .../ui/tree/azureArcTreeDataProvider.test.ts | 10 +- .../postgres/postgresOverviewPage.ts | 11 +- extensions/arc/yarn.lock | 8 +- 7 files changed, 205 insertions(+), 101 deletions(-) create mode 100644 extensions/arc/src/test/ui/dashboards/postgresOverviewPage.test.ts diff --git a/extensions/arc/package.json b/extensions/arc/package.json index c6cfbb5dc7..265ed28f3d 100644 --- a/extensions/arc/package.json +++ b/extensions/arc/package.json @@ -1051,7 +1051,7 @@ "@types/sinon": "^9.0.4", "@types/uuid": "^8.3.0", "@types/yamljs": "^0.2.31", - "@microsoft/azdata-test": "^1.4.0", + "@microsoft/azdata-test": "^1.5.0", "mocha": "^5.2.0", "mocha-junit-reporter": "^1.17.0", "mocha-multi-reporters": "^1.1.7", diff --git a/extensions/arc/src/test/mocks/fakeAzdataApi.ts b/extensions/arc/src/test/mocks/fakeAzdataApi.ts index e9c6a95aee..e798a9c701 100644 --- a/extensions/arc/src/test/mocks/fakeAzdataApi.ts +++ b/extensions/arc/src/test/mocks/fakeAzdataApi.ts @@ -10,67 +10,78 @@ import * as azdataExt from 'azdata-ext'; */ export class FakeAzdataApi implements azdataExt.IAzdataApi { - public postgresInstances: azdataExt.PostgresServerListResult[] = []; - public miaaInstances: azdataExt.SqlMiListResult[] = []; + private _arcApi = { + dc: { + create(_namespace: string, _name: string, _connectivityMode: string, _resourceGroup: string, _location: string, _subscription: string, _profileName?: string, _storageClass?: string): Promise> { throw new Error('Method not implemented.'); }, + endpoint: { + async list(): Promise> { return { result: [] }; } + }, + config: { + list(): Promise> { throw new Error('Method not implemented.'); }, + async show(): Promise> { return { result: undefined! }; } + } + }, + postgres: { + server: { + postgresInstances: [], + delete(_name: string): Promise> { throw new Error('Method not implemented.'); }, + async list(): Promise> { return { result: this.postgresInstances }; }, + show(_name: string): Promise> { throw new Error('Method not implemented.'); }, + edit( + _name: string, + _args: { + adminPassword?: boolean, + coresLimit?: string, + coresRequest?: string, + engineSettings?: string, + extensions?: string, + memoryLimit?: string, + memoryRequest?: string, + noWait?: boolean, + port?: number, + replaceEngineSettings?: boolean, + workers?: number + }, + _engineVersion?: string, + _additionalEnvVars?: azdataExt.AdditionalEnvVars + ): Promise> { throw new Error('Method not implemented.'); } + } + }, + sql: { + mi: { + miaaInstances: [], + delete(_name: string): Promise> { throw new Error('Method not implemented.'); }, + async list(): Promise> { return { result: this.miaaInstances }; }, + show(_name: string): Promise> { throw new Error('Method not implemented.'); }, + edit( + _name: string, + _args: { + coresLimit?: string, + coresRequest?: string, + memoryLimit?: string, + memoryRequest?: string, + noWait?: boolean + }): Promise> { throw new Error('Method not implemented.'); } + } + } + }; + + // public postgresInstances: azdataExt.PostgresServerListResult[] = []; + public set postgresInstances(instances: azdataExt.PostgresServerListResult[]) { + this._arcApi.postgres.server.postgresInstances = instances; + } + + public set miaaInstances(instances: azdataExt.SqlMiListResult[]) { + this._arcApi.sql.mi.miaaInstances = instances; + } + + // public miaaInstances: azdataExt.SqlMiListResult[] = []; // // API Implementation // public get arc() { - const self = this; - return { - dc: { - create(_namespace: string, _name: string, _connectivityMode: string, _resourceGroup: string, _location: string, _subscription: string, _profileName?: string, _storageClass?: string): Promise> { throw new Error('Method not implemented.'); }, - endpoint: { - async list(): Promise> { return { result: [] }; } - }, - config: { - list(): Promise> { throw new Error('Method not implemented.'); }, - async show(): Promise> { return { result: undefined! }; } - } - }, - postgres: { - server: { - delete(_name: string): Promise> { throw new Error('Method not implemented.'); }, - async list(): Promise> { return { result: self.postgresInstances }; }, - show(_name: string): Promise> { throw new Error('Method not implemented.'); }, - edit( - _name: string, - _args: { - adminPassword?: boolean, - coresLimit?: string, - coresRequest?: string, - engineSettings?: string, - extensions?: string, - memoryLimit?: string, - memoryRequest?: string, - noWait?: boolean, - port?: number, - replaceEngineSettings?: boolean, - workers?: number - }, - _engineVersion?: string, - _additionalEnvVars?: azdataExt.AdditionalEnvVars - ): Promise> { throw new Error('Method not implemented.'); } - } - }, - sql: { - mi: { - delete(_name: string): Promise> { throw new Error('Method not implemented.'); }, - async list(): Promise> { return { result: self.miaaInstances }; }, - show(_name: string): Promise> { throw new Error('Method not implemented.'); }, - edit( - _name: string, - _args: { - coresLimit?: string, - coresRequest?: string, - memoryLimit?: string, - memoryRequest?: string, - noWait?: boolean - }): Promise> { throw new Error('Method not implemented.'); } - } - } - }; + return this._arcApi; } getPath(): Promise { throw new Error('Method not implemented.'); diff --git a/extensions/arc/src/test/models/postgresModel.test.ts b/extensions/arc/src/test/models/postgresModel.test.ts index 2445026a1e..9df463cd02 100644 --- a/extensions/arc/src/test/models/postgresModel.test.ts +++ b/extensions/arc/src/test/models/postgresModel.test.ts @@ -18,7 +18,6 @@ import { ConnectToPGSqlDialog } from '../../ui/dialogs/connectPGDialog'; import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider'; import { FakeControllerModel } from '../mocks/fakeControllerModel'; import { FakeAzdataApi } from '../mocks/fakeAzdataApi'; -import { assert } from 'sinon'; export const FakePostgresServerShowOutput: azdataExt.AzdataOutput = { logs: [], @@ -134,46 +133,34 @@ describe('PostgresModel', function (): void { }); it('Updates model to expected config', async function (): Promise { - const postgresShow = sinon.stub().returns(FakePostgresServerShowOutput); - sinon.stub(azdataApi, 'arc').get(() => { - return { postgres: { server: { show(name: string) { return postgresShow(name); } } } }; - }); + const postgresShowStub = sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput); await postgresModel.refresh(); - sinon.assert.calledOnceWithExactly(postgresShow, postgresModel.info.name); - assert.match(postgresModel.config, FakePostgresServerShowOutput.result); + sinon.assert.calledOnceWithExactly(postgresShowStub, postgresModel.info.name, sinon.match.any, sinon.match.any); + sinon.assert.match(postgresModel.config, FakePostgresServerShowOutput.result); }); it('Updates onConfigLastUpdated when model is refreshed', async function (): Promise { - const postgresShow = sinon.stub().returns(FakePostgresServerShowOutput); - sinon.stub(azdataApi, 'arc').get(() => { - return { postgres: { server: { show(name: string) { return postgresShow(name); } } } }; - }); + const postgresShowStub = sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput); await postgresModel.refresh(); - sinon.assert.calledOnceWithExactly(postgresShow, postgresModel.info.name); + sinon.assert.calledOnceWithExactly(postgresShowStub, postgresModel.info.name, sinon.match.any, sinon.match.any); should(postgresModel.configLastUpdated).be.Date(); }); it('Calls onConfigUpdated event when model is refreshed', async function (): Promise { - const postgresShow = sinon.stub().returns(FakePostgresServerShowOutput); - sinon.stub(azdataApi, 'arc').get(() => { - return { postgres: { server: { show(name: string) { return postgresShow(name); } } } }; - }); + const postgresShowStub = sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput); const configUpdatedEvent = sinon.spy(vscode.EventEmitter.prototype, 'fire'); await postgresModel.refresh(); - sinon.assert.calledOnceWithExactly(postgresShow, postgresModel.info.name); + sinon.assert.calledOnceWithExactly(postgresShowStub, postgresModel.info.name, sinon.match.any, sinon.match.any); sinon.assert.calledOnceWithExactly(configUpdatedEvent, postgresModel.config); }); it('Expected exception is thrown', async function (): Promise { // Stub 'azdata arc postgres server show' to throw an exception - const error = new Error("something bad happened"); - const postgresShow = sinon.stub().throws(error); - sinon.stub(azdataApi, 'arc').get(() => { - return { postgres: { server: { show(name: string) { return postgresShow(name); } } } }; - }); + const error = new Error('something bad happened'); + sinon.stub(azdataApi.arc.postgres.server, 'show').throws(error); await should(postgresModel.refresh()).be.rejectedWith(error); }); @@ -187,11 +174,7 @@ describe('PostgresModel', function (): void { const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances }; postgresModel = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType().object)); - //Stub calling refresh postgres model - const postgresShow = sinon.stub().returns(FakePostgresServerShowOutput); - sinon.stub(azdataApi, 'arc').get(() => { - return { postgres: { server: { show(name: string) { return postgresShow(name); } } } }; - }); + sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput); //Call to provide external endpoint await postgresModel.refresh(); @@ -360,10 +343,7 @@ describe('PostgresModel', function (): void { postgresModel = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType().object)); //Stub calling refresh postgres model - const postgresShow = sinon.stub().returns(FakePostgresServerShowOutput); - sinon.stub(azdataApi, 'arc').get(() => { - return { postgres: { server: { show(name: string) { return postgresShow(name); } } } }; - }); + sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput); //Stub how to get connection profile const iconnectionProfileMock = TypeMoq.Mock.ofType(); diff --git a/extensions/arc/src/test/ui/dashboards/postgresOverviewPage.test.ts b/extensions/arc/src/test/ui/dashboards/postgresOverviewPage.test.ts new file mode 100644 index 0000000000..666c8637e4 --- /dev/null +++ b/extensions/arc/src/test/ui/dashboards/postgresOverviewPage.test.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import * as azdataExt from 'azdata-ext'; +import * as utils from '../../../common/utils'; +import * as loc from '../../../localizedConstants'; +import { Deferred } from '../../../common/promise'; +import { createModelViewMock } from '@microsoft/azdata-test/out/mocks/modelView/modelViewMock'; +import { StubButton } from '@microsoft/azdata-test/out/stubs/modelView/stubButton'; +import { PGResourceInfo, ResourceType } from 'arc'; +import { PostgresOverviewPage } from '../../../ui/dashboards/postgres/postgresOverviewPage'; +import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider'; +import { FakeControllerModel } from '../../mocks/fakeControllerModel'; +import { FakeAzdataApi } from '../../mocks/fakeAzdataApi'; +import { PostgresModel } from '../../../models/postgresModel'; +import { ControllerModel, Registration } from '../../../models/controllerModel'; + +describe('postgresOverviewPage', () => { + let postgresOverview: PostgresOverviewPage; + let azdataApi: azdataExt.IAzdataApi; + let controllerModel: ControllerModel; + let postgresModel: PostgresModel; + + let showInformationMessage: sinon.SinonStub; + let showErrorMessage: sinon.SinonStub; + + let informationMessageShown: Deferred; + let errorMessageShown: Deferred; + + beforeEach(async () => { + // Stub the azdata CLI API + azdataApi = new FakeAzdataApi(); + const azdataExt = TypeMoq.Mock.ofType(); + azdataExt.setup(x => x.azdata).returns(() => azdataApi); + sinon.stub(vscode.extensions, 'getExtension').returns({ exports: azdataExt.object }); + + // Stub the window UI + informationMessageShown = new Deferred(); + showInformationMessage = sinon.stub(vscode.window, 'showInformationMessage').callsFake( + (_: string, __: vscode.MessageOptions, ...___: vscode.MessageItem[]) => { + informationMessageShown.resolve(); + return Promise.resolve(undefined); + }); + + errorMessageShown = new Deferred(); + showErrorMessage = sinon.stub(vscode.window, 'showErrorMessage').callsFake( + (_: string, __: vscode.MessageOptions, ...___: vscode.MessageItem[]) => { + errorMessageShown.resolve(); + return Promise.resolve(undefined); + }); + + // Setup the PostgresModel + controllerModel = new FakeControllerModel(); + const postgresResource: PGResourceInfo = { name: 'my-pg', resourceType: '' }; + const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances }; + const treeDataProvider = new AzureArcTreeDataProvider(TypeMoq.Mock.ofType().object); + postgresModel = new PostgresModel(controllerModel, postgresResource, registration, treeDataProvider); + + // Setup the PostgresOverviewPage + const { modelViewMock } = createModelViewMock(); + postgresOverview = new PostgresOverviewPage(modelViewMock.object, controllerModel, postgresModel); + // Call the getter to initialize toolbar, but we don't need to use it for anything + // eslint-disable-next-line code-no-unused-expressions + postgresOverview['toolbarContainer']; + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('delete button', () => { + let refreshTreeNode: sinon.SinonStub; + + beforeEach(() => { + sinon.stub(utils, 'promptForInstanceDeletion').returns(Promise.resolve(true)); + sinon.stub(controllerModel, 'acquireAzdataSession').returns(Promise.resolve(vscode.Disposable.from())); + refreshTreeNode = sinon.stub(controllerModel, 'refreshTreeNode'); + }); + + it('deletes Postgres on success', async () => { + // Stub 'azdata arc postgres server delete' to return success + const postgresDeleteStub = sinon.stub(azdataApi.arc.postgres.server, 'delete'); + + (postgresOverview['deleteButton'] as StubButton).click(); + await informationMessageShown; + sinon.assert.calledOnceWithExactly(postgresDeleteStub, postgresModel.info.name, sinon.match.any, sinon.match.any); + sinon.assert.calledOnceWithExactly(showInformationMessage, loc.instanceDeleted(postgresModel.info.name)); + sinon.assert.notCalled(showErrorMessage); + sinon.assert.calledOnce(refreshTreeNode); + }); + + it('shows an error message on failure', async () => { + // Stub 'azdata arc postgres server delete' to throw an exception + const error = new Error('something bad happened'); + const postgresDeleteStub = sinon.stub(azdataApi.arc.postgres.server, 'delete').throws(error); + + (postgresOverview['deleteButton'] as StubButton).click(); + await errorMessageShown; + sinon.assert.calledOnceWithExactly(postgresDeleteStub, postgresModel.info.name, sinon.match.any, sinon.match.any); + sinon.assert.notCalled(showInformationMessage); + sinon.assert.calledOnceWithExactly(showErrorMessage, loc.instanceDeletionFailed(postgresModel.info.name, error.message)); + sinon.assert.notCalled(refreshTreeNode); + }); + }); +}); diff --git a/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts b/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts index 674915e6fb..6952f0120d 100644 --- a/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts +++ b/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts @@ -102,8 +102,10 @@ describe('AzureArcTreeDataProvider tests', function (): void { return mockArcApi.object; }); const fakeAzdataApi = new FakeAzdataApi(); - fakeAzdataApi.postgresInstances = [{ name: 'pg1', state: '', workers: 0 }]; - fakeAzdataApi.miaaInstances = [{ name: 'miaa1', state: '', replicas: '', serverEndpoint: '' }]; + const pgInstances = [{ name: 'pg1', state: '', workers: 0 }]; + const miaaInstances = [{ name: 'miaa1', state: '', replicas: '', serverEndpoint: '' }]; + fakeAzdataApi.postgresInstances = pgInstances; + fakeAzdataApi.miaaInstances = miaaInstances; mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi); sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object); @@ -112,8 +114,8 @@ describe('AzureArcTreeDataProvider tests', function (): void { await treeDataProvider.addOrUpdateController(controllerModel, ''); const controllerNode = treeDataProvider.getControllerNode(controllerModel); const children = await treeDataProvider.getChildren(controllerNode); - should(children.filter(c => c.label === fakeAzdataApi.postgresInstances[0].name).length).equal(1, 'Should have a Postgres child'); - should(children.filter(c => c.label === fakeAzdataApi.miaaInstances[0].name).length).equal(1, 'Should have a MIAA child'); + should(children.filter(c => c.label === pgInstances[0].name).length).equal(1, 'Should have a Postgres child'); + should(children.filter(c => c.label === miaaInstances[0].name).length).equal(1, 'Should have a MIAA child'); should(children.length).equal(2, 'Should have exactly 2 children'); }); }); diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts index da3a3ce8cb..8db7bb769c 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts @@ -30,6 +30,7 @@ export class PostgresOverviewPage extends DashboardPage { private properties!: azdata.PropertiesContainerComponent; private kibanaLink!: azdata.HyperlinkComponent; private grafanaLink!: azdata.HyperlinkComponent; + private deleteButton!: azdata.ButtonComponent; private podStatusTable!: azdata.DeclarativeTableComponent; private podStatusData: PodStatusModel[] = []; @@ -241,14 +242,14 @@ export class PostgresOverviewPage extends DashboardPage { })); // Delete service - const deleteButton = this.modelView.modelBuilder.button().withProperties({ + this.deleteButton = this.modelView.modelBuilder.button().withProperties({ label: loc.deleteText, iconPath: IconPathHelper.delete }).component(); this.disposables.push( - deleteButton.onDidClick(async () => { - deleteButton.enabled = false; + this.deleteButton.onDidClick(async () => { + this.deleteButton.enabled = false; try { if (await promptForInstanceDeletion(this._postgresModel.info.name)) { await vscode.window.withProgress( @@ -273,7 +274,7 @@ export class PostgresOverviewPage extends DashboardPage { } catch (error) { vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._postgresModel.info.name, error)); } finally { - deleteButton.enabled = true; + this.deleteButton.enabled = true; } })); @@ -323,7 +324,7 @@ export class PostgresOverviewPage extends DashboardPage { return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ { component: resetPasswordButton }, - { component: deleteButton }, + { component: this.deleteButton }, { component: refreshButton, toolbarSeparatorAfter: true }, { component: openInAzurePortalButton } ]).component(); diff --git a/extensions/arc/yarn.lock b/extensions/arc/yarn.lock index 305ad92020..cc4c2b7be2 100644 --- a/extensions/arc/yarn.lock +++ b/extensions/arc/yarn.lock @@ -182,10 +182,10 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@microsoft/azdata-test@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@microsoft/azdata-test/-/azdata-test-1.4.0.tgz#a809187ae8a065c518e3a3e2d350883e592853bc" - integrity sha512-iscDA13/XRknRCNauP9OPsSg/ulTrMJOPFA0XMyNG1it3zY8mEJxxFJcNkWTnnEWpOUFvyksvoouzYUNy1fvrQ== +"@microsoft/azdata-test@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@microsoft/azdata-test/-/azdata-test-1.5.0.tgz#5ffa9ec6b704fea439c63d7dfa46dcfcf3236747" + integrity sha512-kaDn5geXqrhcZgxCWXSrbXdUpJi5TFmi+sIPDfmhMYJa8uecn9C2rzxn5ZbxBN5cjjYOWF318dERfe+S0CWnlA== dependencies: http-proxy-agent "^2.1.0" https-proxy-agent "^2.2.4"