diff --git a/extensions/arc/src/test/mocks/fakeAzdataApi.ts b/extensions/arc/src/test/mocks/fakeAzdataApi.ts new file mode 100644 index 0000000000..7312e948f5 --- /dev/null +++ b/extensions/arc/src/test/mocks/fakeAzdataApi.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdataExt from 'azdata-ext'; + +/** + * Simple fake Azdata Api used to mock the API during tests + */ +export class FakeAzdataApi implements azdataExt.IAzdataApi { + + public postgresInstances: azdataExt.PostgresServerListResult[] = []; + 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 + }, + _additionalEnvVars?: { [key: string]: string }): 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.'); } + } + } + }; + } + getPath(): Promise { + throw new Error('Method not implemented.'); + } + login(_endpoint: string, _username: string, _password: string): Promise> { + return undefined; + } + version(): Promise> { + throw new Error('Method not implemented.'); + } + getSemVersion(): any { + throw new Error('Method not implemented.'); + } + +} diff --git a/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts b/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts index a6161223f0..3d429de41a 100644 --- a/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts +++ b/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts @@ -3,16 +3,21 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ControllerInfo } from 'arc'; +import { ControllerInfo, ResourceType } from 'arc'; import 'mocha'; import * as should from 'should'; import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; import { v4 as uuid } from 'uuid'; import * as vscode from 'vscode'; +import * as azdataExt from 'azdata-ext'; import { ControllerModel } from '../../../models/controllerModel'; +import { MiaaModel } from '../../../models/miaaModel'; import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider'; import { ControllerTreeNode } from '../../../ui/tree/controllerTreeNode'; +import { MiaaTreeNode } from '../../../ui/tree/miaaTreeNode'; import { FakeControllerModel } from '../../mocks/fakeControllerModel'; +import { FakeAzdataApi } from '../../mocks/fakeAzdataApi'; describe('AzureArcTreeDataProvider tests', function (): void { let treeDataProvider: AzureArcTreeDataProvider; @@ -84,6 +89,27 @@ describe('AzureArcTreeDataProvider tests', function (): void { let children = await treeDataProvider.getChildren(); should(children.length).equal(0, 'After loading we should have 0 children'); }); + + it('should return all children of controller after loading', async function (): Promise { + const mockArcExtension = TypeMoq.Mock.ofType>(); + const mockArcApi = TypeMoq.Mock.ofType(); + mockArcExtension.setup(x => x.exports).returns(() => { + return mockArcApi.object; + }); + const fakeAzdataApi = new FakeAzdataApi(); + fakeAzdataApi.postgresInstances = [{ name: 'pg1', state: '', workers: 0 }]; + fakeAzdataApi.miaaInstances = [{ name: 'miaa1', state: '', replicas: '', serverEndpoint: '' }]; + mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi); + + sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object); + const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'mypassword'); + 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.length).equal(2, 'Should have excatly 2 children'); + }); }); describe('removeController', function (): void { @@ -104,4 +130,31 @@ describe('AzureArcTreeDataProvider tests', function (): void { should((await treeDataProvider.getChildren()).length).equal(0, 'Removing other node again should do nothing'); }); }); + + describe('openResourceDashboard', function (): void { + it('Opening dashboard for nonexistent controller node throws', async function (): Promise { + const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); + const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, ''); + await should(openDashboardPromise).be.rejected(); + }); + + it('Opening dashboard for nonexistent resource throws', async function (): Promise { + const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); + await treeDataProvider.addOrUpdateController(controllerModel, ''); + const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, ''); + await should(openDashboardPromise).be.rejected(); + }); + + it('Opening dashboard for existing resource node succeeds', async function (): Promise { + const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); + const miaaModel = new MiaaModel(controllerModel, { name: 'miaa-1', resourceType: ResourceType.sqlManagedInstances }, undefined!, treeDataProvider); + await treeDataProvider.addOrUpdateController(controllerModel, ''); + const controllerNode = treeDataProvider.getControllerNode(controllerModel)!; + const resourceNode = new MiaaTreeNode(miaaModel, controllerModel); + sinon.stub(controllerNode, 'getResourceNode').returns(resourceNode); + const showDashboardStub = sinon.stub(resourceNode, 'openDashboard'); + await treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, ''); + should(showDashboardStub.calledOnce).be.true('showDashboard should have been called exactly once'); + }); + }); }); diff --git a/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts b/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts index cd92471c70..35c7a85a8e 100644 --- a/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts +++ b/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts @@ -135,10 +135,14 @@ export class AzureArcTreeDataProvider implements vscode.TreeDataProvider[] = []; constructor(public model: ControllerModel, private _context: vscode.ExtensionContext, private _treeDataProvider: AzureArcTreeDataProvider) { super(model.label, vscode.TreeItemCollapsibleState.Collapsed, ResourceType.dataControllers); @@ -69,14 +70,14 @@ export class ControllerTreeNode extends TreeNode { * @param resourceType The resourceType of the node * @param name The name of the node */ - public getResourceNode(resourceType: string, name: string): ResourceTreeNode | undefined { + public getResourceNode(resourceType: string, name: string): ResourceTreeNode | undefined { return this._children.find(c => c.model?.info.resourceType === resourceType && c.model.info.name === name); } private updateChildren(registrations: Registration[]): void { - const newChildren: ResourceTreeNode[] = []; + const newChildren: ResourceTreeNode[] = []; registrations.forEach(registration => { if (!registration.instanceName) { console.warn('Registration is missing required name value, skipping'); diff --git a/extensions/arc/src/ui/tree/miaaTreeNode.ts b/extensions/arc/src/ui/tree/miaaTreeNode.ts index 4e8556874e..cd6509b46c 100644 --- a/extensions/arc/src/ui/tree/miaaTreeNode.ts +++ b/extensions/arc/src/ui/tree/miaaTreeNode.ts @@ -8,15 +8,15 @@ import * as vscode from 'vscode'; import { ControllerModel } from '../../models/controllerModel'; import { MiaaModel } from '../../models/miaaModel'; import { MiaaDashboard } from '../dashboards/miaa/miaaDashboard'; -import { TreeNode } from './treeNode'; +import { ResourceTreeNode } from './resourceTreeNode'; /** * The TreeNode for displaying a SQL Managed Instance on Azure Arc */ -export class MiaaTreeNode extends TreeNode { +export class MiaaTreeNode extends ResourceTreeNode { - constructor(public model: MiaaModel, private _controllerModel: ControllerModel) { - super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.sqlManagedInstances); + constructor(model: MiaaModel, private _controllerModel: ControllerModel) { + super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.sqlManagedInstances, model); } public async openDashboard(): Promise { diff --git a/extensions/arc/src/ui/tree/postgresTreeNode.ts b/extensions/arc/src/ui/tree/postgresTreeNode.ts index 17bd523050..2d3dcb5633 100644 --- a/extensions/arc/src/ui/tree/postgresTreeNode.ts +++ b/extensions/arc/src/ui/tree/postgresTreeNode.ts @@ -13,14 +13,14 @@ import { ResourceTreeNode } from './resourceTreeNode'; /** * The TreeNode for displaying an Postgres Server group */ -export class PostgresTreeNode extends ResourceTreeNode { +export class PostgresTreeNode extends ResourceTreeNode { - constructor(private _model: PostgresModel, private _controllerModel: ControllerModel, private _context: vscode.ExtensionContext) { - super(_model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.postgresInstances, _model); + constructor(model: PostgresModel, private _controllerModel: ControllerModel, private _context: vscode.ExtensionContext) { + super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.postgresInstances, model); } public async openDashboard(): Promise { - const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this._model); + const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this.model); await postgresDashboard.showDashboard(); } } diff --git a/extensions/arc/src/ui/tree/resourceTreeNode.ts b/extensions/arc/src/ui/tree/resourceTreeNode.ts index 834dbd6467..b916bafe69 100644 --- a/extensions/arc/src/ui/tree/resourceTreeNode.ts +++ b/extensions/arc/src/ui/tree/resourceTreeNode.ts @@ -10,8 +10,8 @@ import { TreeNode } from './treeNode'; /** * A TreeNode belonging to a child of a Controller */ -export abstract class ResourceTreeNode extends TreeNode { - constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, resourceType?: string, public model?: ResourceModel) { +export abstract class ResourceTreeNode extends TreeNode { + constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, resourceType: string, public model: M) { super(label, collapsibleState, resourceType); } }