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 <brberger@microsoft.com>
Co-authored-by: chgagnon <chgagnon@microsoft.com>
This commit is contained in:
Brian Bergeron
2021-03-19 13:12:26 -07:00
committed by GitHub
parent 69d593007e
commit ce39b4bd19
7 changed files with 205 additions and 101 deletions

View File

@@ -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",

View File

@@ -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<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
endpoint: {
async list(): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> { return <any>{ result: [] }; }
},
config: {
list(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> { throw new Error('Method not implemented.'); },
async show(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> { return <any>{ result: undefined! }; }
}
},
postgres: {
server: {
postgresInstances: [],
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> { return <any>{ result: this.postgresInstances }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> { 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<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
},
sql: {
mi: {
miaaInstances: [],
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> { return <any>{ result: this.miaaInstances }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> { throw new Error('Method not implemented.'); },
edit(
_name: string,
_args: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean
}): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
}
};
// public postgresInstances: azdataExt.PostgresServerListResult[] = [];
public set postgresInstances(instances: azdataExt.PostgresServerListResult[]) {
this._arcApi.postgres.server.postgresInstances = <any>instances;
}
public set miaaInstances(instances: azdataExt.SqlMiListResult[]) {
this._arcApi.sql.mi.miaaInstances = <any>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<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
endpoint: {
async list(): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> { return <any>{ result: [] }; }
},
config: {
list(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> { throw new Error('Method not implemented.'); },
async show(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> { return <any>{ result: undefined! }; }
}
},
postgres: {
server: {
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> { return <any>{ result: self.postgresInstances }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> { 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<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
},
sql: {
mi: {
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> { return <any>{ result: self.miaaInstances }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> { throw new Error('Method not implemented.'); },
edit(
_name: string,
_args: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean
}): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
}
};
return this._arcApi;
}
getPath(): Promise<string> {
throw new Error('Method not implemented.');

View File

@@ -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<azdataExt.PostgresServerShowResult> = {
logs: [],
@@ -134,46 +133,34 @@ describe('PostgresModel', function (): void {
});
it('Updates model to expected config', async function (): Promise<void> {
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<void> {
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<void> {
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<void> {
// 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<vscode.ExtensionContext>().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<vscode.ExtensionContext>().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<azdata.IConnectionProfile>();

View File

@@ -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.IExtension>();
azdataExt.setup(x => x.azdata).returns(() => azdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ 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<vscode.ExtensionContext>().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);
});
});
});

View File

@@ -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');
});
});

View File

@@ -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<azdata.ButtonProperties>({
this.deleteButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
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();

View File

@@ -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"