diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json index 6615f51fa8..a18be62e36 100644 --- a/extensions/azurecore/package.json +++ b/extensions/azurecore/package.json @@ -17,12 +17,12 @@ "configuration": [ { "type": "object", - "title": "%azure.config.title%", + "title": "%azure.resource.config.title%", "properties": { - "azureResource.resourceFilter": { + "azure.resource.config.filter": { "type": "array", "default": null, - "description": "%azure.resourceFilter.description%" + "description": "%azure.resource.config.filter.description%" } } }, @@ -61,39 +61,47 @@ "category": "Azure Accounts" }, { - "command": "azureresource.refreshall", - "title": "%azureresource.refreshall%", + "command": "azure.resource.refreshall", + "title": "%azure.resource.refreshall.title%", "icon": { "dark": "resources/dark/refresh_inverse.svg", "light": "resources/light/refresh.svg" } }, { - "command": "azureresource.refresh", - "title": "%azureresource.refresh%", + "command": "azure.resource.refresh", + "title": "%azure.resource.refresh.title%", "icon": { "dark": "resources/dark/refresh_inverse.svg", "light": "resources/light/refresh.svg" } }, { - "command": "azureresource.signin", - "title": "%azureresource.signin%" + "command": "azure.resource.signin", + "title": "%azure.resource.signin.title%" }, { - "command": "azureresource.connectsqldb", - "title": "%azureresource.connectsqldb%", + "command": "azure.resource.selectsubscriptions", + "title": "%azure.resource.selectsubscriptions.title%", + "icon": { + "dark": "resources/dark/filter_inverse.svg", + "light": "resources/light/filter.svg" + } + }, + { + "command": "azure.resource.connectsqlserver", + "title": "%azure.resource.connectsqlserver.title%", "icon": { "dark": "resources/dark/connect_to_inverse.svg", "light": "resources/light/connect_to.svg" } }, { - "command": "azureresource.selectsubscriptions", - "title": "%azureresource.selectsubscriptions%", + "command": "azure.resource.connectsqldb", + "title": "%azure.resource.connectsqldb.title%", "icon": { - "dark": "resources/dark/filter_inverse.svg", - "light": "resources/light/filter.svg" + "dark": "resources/dark/connect_to_inverse.svg", + "light": "resources/light/connect_to.svg" } } ], @@ -110,46 +118,47 @@ "azureResource": [ { "id": "azureResourceExplorer", - "name": "%azure.resourceExplorer.title%" + "name": "%azure.resource.explorer.title%" } ] }, "menus": { "view/title": [ { - "command": "azureresource.refreshall", + "command": "azure.resource.refreshall", "when": "view == azureResourceExplorer", "group": "navigation@1" } ], "view/item/context": [ { - "command": "azureresource.connectsqldb", - "when": "viewItem =~ /^azureResource\\.itemType\\.database(?:Server){0,1}$/", - "group": "1azureresource@1" - }, - { - "command": "azureresource.connectsqldb", - "when": "viewItem =~ /^azureResource\\.itemType\\.database(?:Server){0,1}$/", + "command": "azure.resource.selectsubscriptions", + "when": "viewItem == azure.resource.itemType.account", "group": "inline" }, { - "command": "azureresource.selectsubscriptions", - "when": "viewItem == azureResource.itemType.account", + "command": "azure.resource.refresh", + "when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer)$/", "group": "inline" }, { - "command": "azureresource.refresh", - "when": "viewItem =~ /^azureResource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer)$/", + "command": "azure.resource.connectsqlserver", + "when": "viewItem == azure.resource.itemType.databaseServer", + "group": "inline" + }, + { + "command": "azure.resource.connectsqldb", + "when": "viewItem == azure.resource.itemType.database", "group": "inline" } ] - } + }, + "hasAzureResourceProviders": true }, "dependencies": { - "request": "2.88.0", "azure-arm-resource": "^7.0.0", "azure-arm-sql": "^5.0.1", + "request": "2.88.0", "vscode-nls": "^4.0.0" }, "devDependencies": { @@ -157,6 +166,7 @@ "@types/node": "^8.0.24", "mocha": "^5.2.0", "should": "^13.2.1", + "vscode": "^1.1.26", "typemoq": "^2.1.0" } -} \ No newline at end of file +} diff --git a/extensions/azurecore/package.nls.json b/extensions/azurecore/package.nls.json index 5af2a7968d..ce802b4365 100644 --- a/extensions/azurecore/package.nls.json +++ b/extensions/azurecore/package.nls.json @@ -1,16 +1,20 @@ { "azure.displayName": "Azure (Core)", "azure.description": "Browse and work with Azure resources", - "azure.config.title": "Azure Resource Configuration", - "azure.resourceFilter.description": "The resource filter, each element is an account id, a subscription id and name separated by a slash", - "azureresource.refreshall": "Refresh All", - "azureresource.refresh": "Refresh", - "azureresource.signin": "Sign In", - "azureresource.connectsqldb": "Connect", - "azureresource.selectsubscriptions": "Select Subscriptions", "azure.title": "Azure", - "azure.resourceExplorer.title": "Resource Explorer", + + "azure.resource.config.title": "Azure Resource Configuration", + "azure.resource.config.filter.description": "The resource filter, each element is an account id, a subscription id and name separated by a slash", + "azure.resource.explorer.title": "Resource Explorer", + "azure.resource.refreshall.title": "Refresh All", + "azure.resource.refresh.title": "Refresh", + "azure.resource.signin.title": "Sign In", + "azure.resource.selectsubscriptions.title": "Select Subscriptions", + "azure.resource.connectsqlserver.title": "Connect", + "azure.resource.connectsqldb.title": "Connect", + "accounts.clearTokenCache": "Clear Azure Account Token Cache", + "config.enablePublicCloudDescription": "Should Azure public cloud integration be enabled", "config.enableUsGovCloudDescription": "Should US Government Azure cloud (Fairfax) integration be enabled", "config.enableChinaCloudDescription": "Should Azure China integration be enabled", diff --git a/extensions/azurecore/src/azureResource/azure-resource.d.ts b/extensions/azurecore/src/azureResource/azure-resource.d.ts new file mode 100644 index 0000000000..60546165a6 --- /dev/null +++ b/extensions/azurecore/src/azureResource/azure-resource.d.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TreeDataProvider, TreeItem } from 'vscode'; +import { DataProvider, Account } from 'sqlops'; + +export namespace azureResource { + export interface IAzureResourceProvider extends DataProvider { + getTreeDataProvider(): IAzureResourceTreeDataProvider; + } + + export interface IAzureResourceTreeDataProvider extends TreeDataProvider { + } + + export interface IAzureResourceNode { + readonly account: Account; + readonly subscription: AzureResourceSubscription; + readonly tenantId: string; + readonly treeItem: TreeItem; + } + + export interface AzureResourceSubscription { + id: string; + name: string; + } +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/commands.ts b/extensions/azurecore/src/azureResource/commands.ts index 6337078778..cf9cfc590c 100644 --- a/extensions/azurecore/src/azureResource/commands.ts +++ b/extensions/azurecore/src/azureResource/commands.ts @@ -6,35 +6,48 @@ 'use strict'; import { window, QuickPickItem } from 'vscode'; -import * as sqlops from 'sqlops'; -import { generateGuid } from './utils'; -import { ApiWrapper } from '../apiWrapper'; -import { TreeNode } from '../treeNodes'; +import { AzureResource } from 'sqlops'; +import { TokenCredentials } from 'ms-rest'; +import { AppContext } from '../appContext'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); +import { azureResource } from './azure-resource'; +import { TreeNode } from './treeNode'; +import { AzureResourceCredentialError } from './errors'; import { AzureResourceTreeProvider } from './tree/treeProvider'; -import { AzureResourceDatabaseServerTreeNode } from './tree/databaseServerTreeNode'; -import { AzureResourceDatabaseTreeNode } from './tree/databaseTreeNode'; import { AzureResourceAccountTreeNode } from './tree/accountTreeNode'; -import { AzureResourceServicePool } from './servicePool'; -import { AzureResourceSubscription } from './models'; +import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../azureResource/interfaces'; +import { AzureResourceServiceNames } from './constants'; -export function registerAzureResourceCommands(apiWrapper: ApiWrapper, tree: AzureResourceTreeProvider): void { - apiWrapper.registerCommand('azureresource.selectsubscriptions', async (node?: TreeNode) => { +export function registerAzureResourceCommands(appContext: AppContext, tree: AzureResourceTreeProvider): void { + appContext.apiWrapper.registerCommand('azure.resource.selectsubscriptions', async (node?: TreeNode) => { if (!(node instanceof AzureResourceAccountTreeNode)) { return; } + const subscriptionService = appContext.getService(AzureResourceServiceNames.subscriptionService); + const subscriptionFilterService = appContext.getService(AzureResourceServiceNames.subscriptionFilterService); + const accountNode = node as AzureResourceAccountTreeNode; - const servicePool = AzureResourceServicePool.getInstance(); + const subscriptions = (await accountNode.getCachedSubscriptions()) || []; + if (subscriptions.length === 0) { + try { + const tokens = await this.servicePool.apiWrapper.getSecurityToken(this.account, AzureResource.ResourceManagement); - let subscriptions = await accountNode.getCachedSubscriptions(); - if (!subscriptions || subscriptions.length === 0) { - const credentials = await servicePool.credentialService.getCredentials(accountNode.account, sqlops.AzureResource.ResourceManagement); - subscriptions = await servicePool.subscriptionService.getSubscriptions(accountNode.account, credentials); + for (const tenant of this.account.properties.tenants) { + const token = tokens[tenant.id].token; + const tokenType = tokens[tenant.id].tokenType; + + subscriptions.push(...await subscriptionService.getSubscriptions(accountNode.account, new TokenCredentials(token, tokenType))); + } + } catch (error) { + throw new AzureResourceCredentialError(localize('azure.resource.selectsubscriptions.credentialError', 'Failed to get credential for account {0}. Please refresh the account.', this.account.key.accountId), error); + } } - const selectedSubscriptions = (await servicePool.subscriptionFilterService.getSelectedSubscriptions(accountNode.account)) || []; + let selectedSubscriptions = (await subscriptionFilterService.getSelectedSubscriptions(accountNode.account)) || []; const selectedSubscriptionIds: string[] = []; if (selectedSubscriptions.length > 0) { selectedSubscriptionIds.push(...selectedSubscriptions.map((subscription) => subscription.id)); @@ -43,11 +56,11 @@ export function registerAzureResourceCommands(apiWrapper: ApiWrapper, tree: Azur selectedSubscriptionIds.push(...subscriptions.map((subscription) => subscription.id)); } - interface SubscriptionQuickPickItem extends QuickPickItem { - subscription: AzureResourceSubscription; + interface AzureResourceSubscriptionQuickPickItem extends QuickPickItem { + subscription: azureResource.AzureResourceSubscription; } - const subscriptionItems: SubscriptionQuickPickItem[] = subscriptions.map((subscription) => { + const subscriptionQuickPickItems: AzureResourceSubscriptionQuickPickItem[] = subscriptions.map((subscription) => { return { label: subscription.name, picked: selectedSubscriptionIds.indexOf(subscription.id) !== -1, @@ -55,66 +68,22 @@ export function registerAzureResourceCommands(apiWrapper: ApiWrapper, tree: Azur }; }); - const pickedSubscriptionItems = (await window.showQuickPick(subscriptionItems, { canPickMany: true })); - if (pickedSubscriptionItems && pickedSubscriptionItems.length > 0) { + const selectedSubscriptionQuickPickItems = (await window.showQuickPick(subscriptionQuickPickItems, { canPickMany: true })); + if (selectedSubscriptionQuickPickItems && selectedSubscriptionQuickPickItems.length > 0) { tree.refresh(node, false); - const pickedSubscriptions = pickedSubscriptionItems.map((subscriptionItem) => subscriptionItem.subscription); - await servicePool.subscriptionFilterService.saveSelectedSubscriptions(accountNode.account, pickedSubscriptions); + selectedSubscriptions = selectedSubscriptionQuickPickItems.map((subscriptionItem) => subscriptionItem.subscription); + await subscriptionFilterService.saveSelectedSubscriptions(accountNode.account, selectedSubscriptions); } }); - apiWrapper.registerCommand('azureresource.refreshall', () => tree.notifyNodeChanged(undefined)); + appContext.apiWrapper.registerCommand('azure.resource.refreshall', () => tree.notifyNodeChanged(undefined)); - apiWrapper.registerCommand('azureresource.refresh', async (node?: TreeNode) => { + appContext.apiWrapper.registerCommand('azure.resource.refresh', async (node?: TreeNode) => { tree.refresh(node, true); }); - apiWrapper.registerCommand('azureresource.connectsqldb', async (node?: TreeNode) => { - let connectionProfile: sqlops.IConnectionProfile = { - id: generateGuid(), - connectionName: undefined, - serverName: undefined, - databaseName: undefined, - userName: undefined, - password: '', - authenticationType: undefined, - savePassword: true, - groupFullName: '', - groupId: '', - providerName: undefined, - saveProfile: true, - options: { - } - }; - - if (node instanceof AzureResourceDatabaseServerTreeNode) { - let databaseServer = node.databaseServer; - connectionProfile.connectionName = `connection to '${databaseServer.defaultDatabaseName}' on '${databaseServer.fullName}'`; - connectionProfile.serverName = databaseServer.fullName; - connectionProfile.databaseName = databaseServer.defaultDatabaseName; - connectionProfile.userName = databaseServer.loginName; - connectionProfile.authenticationType = 'SqlLogin'; - connectionProfile.providerName = 'MSSQL'; - } - - if (node instanceof AzureResourceDatabaseTreeNode) { - let database = node.database; - connectionProfile.connectionName = `connection to '${database.name}' on '${database.serverFullName}'`; - connectionProfile.serverName = database.serverFullName; - connectionProfile.databaseName = database.name; - connectionProfile.userName = database.loginName; - connectionProfile.authenticationType = 'SqlLogin'; - connectionProfile.providerName = 'MSSQL'; - } - - const conn = await apiWrapper.openConnectionDialog(undefined, connectionProfile, { saveConnection: true, showDashboard: true }); - if (conn) { - apiWrapper.executeCommand('workbench.view.connections'); - } - }); - - apiWrapper.registerCommand('azureresource.signin', async (node?: TreeNode) => { - apiWrapper.executeCommand('sql.action.accounts.manageLinkedAccount'); + appContext.apiWrapper.registerCommand('azure.resource.signin', async (node?: TreeNode) => { + appContext.apiWrapper.executeCommand('sql.action.accounts.manageLinkedAccount'); }); } diff --git a/extensions/azurecore/src/azureResource/constants.ts b/extensions/azurecore/src/azureResource/constants.ts index 3fb5368927..09ab819749 100644 --- a/extensions/azurecore/src/azureResource/constants.ts +++ b/extensions/azurecore/src/azureResource/constants.ts @@ -6,11 +6,19 @@ 'use strict'; export enum AzureResourceItemType { - account = 'azureResource.itemType.account', - subscription = 'azureResource.itemType.subscription', - databaseContainer = 'azureResource.itemType.databaseContainer', - database = 'azureResource.itemType.database', - databaseServerContainer = 'azureResource.itemType.databaseServerContainer', - databaseServer = 'azureResource.itemType.databaseServer', - message = 'azureResource.itemType.message' + account = 'azure.resource.itemType.account', + subscription = 'azure.resource.itemType.subscription', + databaseContainer = 'azure.resource.itemType.databaseContainer', + database = 'azure.resource.itemType.database', + databaseServerContainer = 'azure.resource.itemType.databaseServerContainer', + databaseServer = 'azure.resource.itemType.databaseServer', + message = 'azure.resource.itemType.message' } + +export enum AzureResourceServiceNames { + cacheService = 'AzureResourceCacheService', + accountService = 'AzureResourceAccountService', + subscriptionService = 'AzureResourceSubscriptionService', + subscriptionFilterService = 'AzureResourceSubscriptionFilterService', + tenantService = 'AzureResourceTenantService' +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/errors.ts b/extensions/azurecore/src/azureResource/errors.ts index c0fe4a842f..5f8894a4e4 100644 --- a/extensions/azurecore/src/azureResource/errors.ts +++ b/extensions/azurecore/src/azureResource/errors.ts @@ -8,7 +8,7 @@ export class AzureResourceCredentialError extends Error { constructor( message: string, - public innerError: Error + public readonly innerError: Error ) { super(message); } diff --git a/extensions/azurecore/src/azureResource/interfaces.ts b/extensions/azurecore/src/azureResource/interfaces.ts index 21c40cb65c..d56283f338 100644 --- a/extensions/azurecore/src/azureResource/interfaces.ts +++ b/extensions/azurecore/src/azureResource/interfaces.ts @@ -6,49 +6,40 @@ 'use strict'; import { ServiceClientCredentials } from 'ms-rest'; -import * as sqlops from 'sqlops'; +import { Account, DidChangeAccountsParams } from 'sqlops'; import { Event } from 'vscode'; -import { AzureResourceSubscription, AzureResourceDatabaseServer, AzureResourceDatabase } from './models'; +import { azureResource } from './azure-resource'; export interface IAzureResourceAccountService { - getAccounts(): Promise; + getAccounts(): Promise; - readonly onDidChangeAccounts: Event; -} - -export interface IAzureResourceCredentialService { - getCredentials(account: sqlops.Account, resource: sqlops.AzureResource): Promise; + readonly onDidChangeAccounts: Event; } export interface IAzureResourceSubscriptionService { - getSubscriptions(account: sqlops.Account, credentials: ServiceClientCredentials[]): Promise; + getSubscriptions(account: Account, credential: ServiceClientCredentials): Promise; } export interface IAzureResourceSubscriptionFilterService { - getSelectedSubscriptions(account: sqlops.Account): Promise; + getSelectedSubscriptions(account: Account): Promise; - saveSelectedSubscriptions(account: sqlops.Account, selectedSubscriptions: AzureResourceSubscription[]): Promise; -} - -export interface IAzureResourceDatabaseServerService { - getDatabaseServers(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise; -} - -export interface IAzureResourceDatabaseService { - getDatabases(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise; + saveSelectedSubscriptions(account: Account, selectedSubscriptions: azureResource.AzureResourceSubscription[]): Promise; } export interface IAzureResourceCacheService { + generateKey(id: string): string; + get(key: string): T | undefined; update(key: string, value: T): void; } -export interface IAzureResourceContextService { - getAbsolutePath(relativePath: string): string; - - executeCommand(commandId: string, ...args: any[]): void; - - showErrorMessage(errorMessage: string): void; +export interface IAzureResourceTenantService { + getTenantId(subscription: azureResource.AzureResourceSubscription): Promise; } + +export interface IAzureResourceNodeWithProviderId { + resourceProviderId: string; + resourceNode: azureResource.IAzureResourceNode; +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/tree/messageTreeNode.ts b/extensions/azurecore/src/azureResource/messageTreeNode.ts similarity index 94% rename from extensions/azurecore/src/azureResource/tree/messageTreeNode.ts rename to extensions/azurecore/src/azureResource/messageTreeNode.ts index edea2e5a76..54a8ba805f 100644 --- a/extensions/azurecore/src/azureResource/tree/messageTreeNode.ts +++ b/extensions/azurecore/src/azureResource/messageTreeNode.ts @@ -7,9 +7,9 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { NodeInfo } from 'sqlops'; -import { TreeNode } from '../../treeNodes'; -import { AzureResourceItemType } from '../constants'; +import { TreeNode } from './treeNode'; +import { AzureResourceItemType } from './constants'; export class AzureResourceMessageTreeNode extends TreeNode { public constructor( diff --git a/extensions/azurecore/src/azureResource/providers/database/commands.ts b/extensions/azurecore/src/azureResource/providers/database/commands.ts new file mode 100644 index 0000000000..2ebd8e0ccb --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/database/commands.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { IConnectionProfile } from 'sqlops'; +import { AppContext } from '../../../appContext'; + +import { TreeNode } from '../../treeNode'; +import { generateGuid } from '../../utils'; +import { AzureResourceItemType } from '../../constants'; +import { IAzureResourceDatabaseNode } from './interfaces'; +import { AzureResourceResourceTreeNode } from '../../resourceTreeNode'; + +export function registerAzureResourceDatabaseCommands(appContext: AppContext): void { + appContext.apiWrapper.registerCommand('azure.resource.connectsqldb', async (node?: TreeNode) => { + if (!node) + { + return; + } + + const treeItem = await node.getTreeItem(); + if (treeItem.contextValue !== AzureResourceItemType.database) { + return; + } + + const resourceNode = (node as AzureResourceResourceTreeNode).resourceNodeWithProviderId.resourceNode; + const database = (resourceNode as IAzureResourceDatabaseNode).database; + + let connectionProfile: IConnectionProfile = { + id: generateGuid(), + connectionName: undefined, + serverName: database.serverFullName, + databaseName: database.name, + userName: database.loginName, + password: '', + authenticationType: 'SqlLogin', + savePassword: true, + groupFullName: '', + groupId: '', + providerName: 'MSSQL', + saveProfile: true, + options: {} + }; + + const conn = await appContext.apiWrapper.openConnectionDialog(undefined, connectionProfile, { saveConnection: true, showDashboard: true }); + if (conn) { + appContext.apiWrapper.executeCommand('workbench.view.connections'); + } + }); +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/providers/database/databaseProvider.ts b/extensions/azurecore/src/azureResource/providers/database/databaseProvider.ts new file mode 100644 index 0000000000..6ef0ccef6d --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/database/databaseProvider.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { ExtensionContext } from 'vscode'; +import { ApiWrapper } from '../../../apiWrapper'; + +import { azureResource } from '../../azure-resource'; +import { IAzureResourceDatabaseService } from './interfaces'; +import { AzureResourceDatabaseTreeDataProvider } from './databaseTreeDataProvider'; + +export class AzureResourceDatabaseProvider implements azureResource.IAzureResourceProvider { + public constructor( + databaseService: IAzureResourceDatabaseService, + apiWrapper: ApiWrapper, + extensionContext: ExtensionContext + ) { + this._databaseService = databaseService; + this._apiWrapper = apiWrapper; + this._extensionContext = extensionContext; + } + + public getTreeDataProvider(): azureResource.IAzureResourceTreeDataProvider { + return new AzureResourceDatabaseTreeDataProvider(this._databaseService, this._apiWrapper, this._extensionContext); + } + + public get providerId(): string { + return 'azure.resource.providers.database'; + } + + private _databaseService: IAzureResourceDatabaseService = undefined; + private _apiWrapper: ApiWrapper = undefined; + private _extensionContext: ExtensionContext = undefined; +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/providers/database/databaseService.ts b/extensions/azurecore/src/azureResource/providers/database/databaseService.ts new file mode 100644 index 0000000000..af39c48ee8 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/database/databaseService.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { ServiceClientCredentials } from 'ms-rest'; +import { SqlManagementClient } from 'azure-arm-sql'; + +import { azureResource } from '../../azure-resource'; +import { IAzureResourceDatabaseService } from './interfaces'; +import { AzureResourceDatabase } from './models'; + +export class AzureResourceDatabaseService implements IAzureResourceDatabaseService { + public async getDatabases(subscription: azureResource.AzureResourceSubscription, credential: ServiceClientCredentials): Promise { + const databases: AzureResourceDatabase[] = []; + const sqlManagementClient = new SqlManagementClient(credential, subscription.id); + const svrs = await sqlManagementClient.servers.list(); + for (const svr of svrs) { + // Extract resource group name from svr.id + const svrIdRegExp = new RegExp(`\/subscriptions\/${subscription.id}\/resourceGroups\/(.+)\/providers\/Microsoft\.Sql\/servers\/${svr.name}`); + if (!svrIdRegExp.test(svr.id)) { + continue; + } + + const founds = svrIdRegExp.exec(svr.id); + const resouceGroup = founds[1]; + + const dbs = await sqlManagementClient.databases.listByServer(resouceGroup, svr.name); + dbs.forEach((db) => databases.push({ + name: db.name, + serverName: svr.name, + serverFullName: svr.fullyQualifiedDomainName, + loginName: svr.administratorLogin + })); + } + + return databases; + } +} diff --git a/extensions/azurecore/src/azureResource/providers/database/databaseTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/database/databaseTreeDataProvider.ts new file mode 100644 index 0000000000..b166ac22f9 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/database/databaseTreeDataProvider.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { AzureResource } from 'sqlops'; +import { TreeItem, TreeItemCollapsibleState, ExtensionContext } from 'vscode'; +import { TokenCredentials } from 'ms-rest'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { azureResource } from '../../azure-resource'; +import { IAzureResourceDatabaseService, IAzureResourceDatabaseNode } from './interfaces'; +import { AzureResourceDatabase } from './models'; +import { AzureResourceItemType } from '../../../azureResource/constants'; +import { ApiWrapper } from '../../../apiWrapper'; + +export class AzureResourceDatabaseTreeDataProvider implements azureResource.IAzureResourceTreeDataProvider { + public constructor( + databaseService: IAzureResourceDatabaseService, + apiWrapper: ApiWrapper, + extensionContext: ExtensionContext + ) { + this._databaseService = databaseService; + this._apiWrapper = apiWrapper; + this._extensionContext = extensionContext; + } + + public getTreeItem(element: azureResource.IAzureResourceNode): TreeItem | Thenable { + return element.treeItem; + } + + public async getChildren(element?: azureResource.IAzureResourceNode): Promise { + if (!element) { + return [this.createContainerNode()]; + } + + const tokens = await this._apiWrapper.getSecurityToken(element.account, AzureResource.ResourceManagement); + const credential = new TokenCredentials(tokens[element.tenantId].token, tokens[element.tenantId].tokenType); + + const databases: AzureResourceDatabase[] = (await this._databaseService.getDatabases(element.subscription, credential)) || []; + + return databases.map((database) => { + account: element.account, + subscription: element.subscription, + tenantId: element.tenantId, + database: database, + treeItem: { + id: `databaseServer_${database.serverFullName}.database_${database.name}`, + label: `${database.name} (${database.serverName})`, + iconPath: { + dark: this._extensionContext.asAbsolutePath('resources/dark/sql_database_inverse.svg'), + light: this._extensionContext.asAbsolutePath('resources/light/sql_database.svg') + }, + collapsibleState: TreeItemCollapsibleState.None, + contextValue: AzureResourceItemType.database + } + }); + } + + private createContainerNode(): azureResource.IAzureResourceNode { + return { + account: undefined, + subscription: undefined, + tenantId: undefined, + treeItem: { + id: AzureResourceDatabaseTreeDataProvider.containerId, + label: AzureResourceDatabaseTreeDataProvider.containerLabel, + iconPath: { + dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'), + light: this._extensionContext.asAbsolutePath('resources/light/folder.svg') + }, + collapsibleState: TreeItemCollapsibleState.Collapsed, + contextValue: AzureResourceItemType.databaseContainer + } + }; + } + + private _databaseService: IAzureResourceDatabaseService = undefined; + private _apiWrapper: ApiWrapper = undefined; + private _extensionContext: ExtensionContext = undefined; + + private static readonly containerId = 'azure.resource.providers.database.treeDataProvider.databaseContainer'; + private static readonly containerLabel = localize('azure.resource.providers.database.treeDataProvider.databaseContainerLabel', 'SQL Databases'); +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/providers/database/interfaces.ts b/extensions/azurecore/src/azureResource/providers/database/interfaces.ts new file mode 100644 index 0000000000..1c8acf384b --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/database/interfaces.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { ServiceClientCredentials } from 'ms-rest'; + +import { azureResource } from '../../azure-resource'; +import { AzureResourceDatabase } from './models'; + +export interface IAzureResourceDatabaseService { + getDatabases(subscription: azureResource.AzureResourceSubscription, credential: ServiceClientCredentials): Promise; +} + +export interface IAzureResourceDatabaseNode extends azureResource.IAzureResourceNode { + readonly database: AzureResourceDatabase; +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/providers/database/models.ts b/extensions/azurecore/src/azureResource/providers/database/models.ts new file mode 100644 index 0000000000..2dab3f9b61 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/database/models.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +export interface AzureResourceDatabase { + name: string; + serverName: string; + serverFullName: string; + loginName: string; +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/providers/databaseServer/commands.ts b/extensions/azurecore/src/azureResource/providers/databaseServer/commands.ts new file mode 100644 index 0000000000..d6ec02b558 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/databaseServer/commands.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { IConnectionProfile } from 'sqlops'; +import { AppContext } from '../../../appContext'; + +import { TreeNode } from '../../treeNode'; +import { generateGuid } from '../../utils'; +import { AzureResourceItemType } from '../../constants'; +import { IAzureResourceDatabaseServerNode } from './interfaces'; +import { AzureResourceResourceTreeNode } from '../../resourceTreeNode'; + +export function registerAzureResourceDatabaseServerCommands(appContext: AppContext): void { + appContext.apiWrapper.registerCommand('azure.resource.connectsqlserver', async (node?: TreeNode) => { + if (!node) + { + return; + } + + const treeItem = await node.getTreeItem(); + if (treeItem.contextValue !== AzureResourceItemType.databaseServer) { + return; + } + + const resourceNode = (node as AzureResourceResourceTreeNode).resourceNodeWithProviderId.resourceNode; + const databaseServer = (resourceNode as IAzureResourceDatabaseServerNode).databaseServer; + + let connectionProfile: IConnectionProfile = { + id: generateGuid(), + connectionName: undefined, + serverName: databaseServer.fullName, + databaseName: databaseServer.defaultDatabaseName, + userName: databaseServer.loginName, + password: '', + authenticationType: 'SqlLogin', + savePassword: true, + groupFullName: '', + groupId: '', + providerName: 'MSSQL', + saveProfile: true, + options: {} + }; + + const conn = await appContext.apiWrapper.openConnectionDialog(undefined, connectionProfile, { saveConnection: true, showDashboard: true }); + if (conn) { + appContext.apiWrapper.executeCommand('workbench.view.connections'); + } + }); +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerProvider.ts b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerProvider.ts new file mode 100644 index 0000000000..f2db96df3c --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerProvider.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { ExtensionContext } from 'vscode'; +import { ApiWrapper } from '../../../apiWrapper'; + +import { azureResource } from '../../azure-resource'; +import { IAzureResourceDatabaseServerService } from './interfaces'; +import { AzureResourceDatabaseServerTreeDataProvider } from './databaseServerTreeDataProvider'; + +export class AzureResourceDatabaseServerProvider implements azureResource.IAzureResourceProvider { + public constructor( + databaseServerService: IAzureResourceDatabaseServerService, + apiWrapper: ApiWrapper, + extensionContext: ExtensionContext + ) { + this._databaseServerService = databaseServerService; + this._apiWrapper = apiWrapper; + this._extensionContext = extensionContext; + } + + public getTreeDataProvider(): azureResource.IAzureResourceTreeDataProvider { + return new AzureResourceDatabaseServerTreeDataProvider(this._databaseServerService, this._apiWrapper, this._extensionContext); + } + + public get providerId(): string { + return 'azure.resource.providers.databaseServer'; + } + + private _databaseServerService: IAzureResourceDatabaseServerService = undefined; + private _apiWrapper: ApiWrapper = undefined; + private _extensionContext: ExtensionContext = undefined; +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerService.ts b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerService.ts new file mode 100644 index 0000000000..5dcfd1b8aa --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerService.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { ServiceClientCredentials } from 'ms-rest'; +import { SqlManagementClient } from 'azure-arm-sql'; + +import { azureResource } from '../../azure-resource'; +import { IAzureResourceDatabaseServerService } from './interfaces'; +import { AzureResourceDatabaseServer } from './models'; + +export class AzureResourceDatabaseServerService implements IAzureResourceDatabaseServerService { + public async getDatabaseServers(subscription: azureResource.AzureResourceSubscription, credential: ServiceClientCredentials): Promise { + const databaseServers: AzureResourceDatabaseServer[] = []; + + const sqlManagementClient = new SqlManagementClient(credential, subscription.id); + const svrs = await sqlManagementClient.servers.list(); + + svrs.forEach((svr) => databaseServers.push({ + name: svr.name, + fullName: svr.fullyQualifiedDomainName, + loginName: svr.administratorLogin, + defaultDatabaseName: 'master' + })); + + return databaseServers; + } +} diff --git a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts new file mode 100644 index 0000000000..b564557fcc --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { AzureResource } from 'sqlops'; +import { TreeItem, TreeItemCollapsibleState, ExtensionContext } from 'vscode'; +import { TokenCredentials } from 'ms-rest'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { azureResource } from '../../azure-resource'; +import { IAzureResourceDatabaseServerService, IAzureResourceDatabaseServerNode } from './interfaces'; +import { AzureResourceDatabaseServer } from './models'; +import { AzureResourceItemType } from '../../../azureResource/constants'; +import { ApiWrapper } from '../../../apiWrapper'; + +export class AzureResourceDatabaseServerTreeDataProvider implements azureResource.IAzureResourceTreeDataProvider { + public constructor( + databaseServerService: IAzureResourceDatabaseServerService, + apiWrapper: ApiWrapper, + extensionContext: ExtensionContext + ) { + this._databaseServerService = databaseServerService; + this._apiWrapper = apiWrapper; + this._extensionContext = extensionContext; + } + + public getTreeItem(element: azureResource.IAzureResourceNode): TreeItem | Thenable { + return element.treeItem; + } + + public async getChildren(element?: azureResource.IAzureResourceNode): Promise { + if (!element) { + return [this.createContainerNode()]; + } + + const tokens = await this._apiWrapper.getSecurityToken(element.account, AzureResource.ResourceManagement); + const credential = new TokenCredentials(tokens[element.tenantId].token, tokens[element.tenantId].tokenType); + + const databaseServers: AzureResourceDatabaseServer[] = (await this._databaseServerService.getDatabaseServers(element.subscription, credential)) || []; + + return databaseServers.map((databaseServer) => { + account: element.account, + subscription: element.subscription, + tenantId: element.tenantId, + databaseServer: databaseServer, + treeItem: { + id: `databaseServer_${databaseServer.name}`, + label: databaseServer.name, + iconPath: { + dark: this._extensionContext.asAbsolutePath('resources/dark/sql_server_inverse.svg'), + light: this._extensionContext.asAbsolutePath('resources/light/sql_server.svg') + }, + collapsibleState: TreeItemCollapsibleState.None, + contextValue: AzureResourceItemType.databaseServer + } + }); + } + + private createContainerNode(): azureResource.IAzureResourceNode { + return { + account: undefined, + subscription: undefined, + tenantId: undefined, + treeItem: { + id: AzureResourceDatabaseServerTreeDataProvider.containerId, + label: AzureResourceDatabaseServerTreeDataProvider.containerLabel, + iconPath: { + dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'), + light: this._extensionContext.asAbsolutePath('resources/light/folder.svg') + }, + collapsibleState: TreeItemCollapsibleState.Collapsed, + contextValue: AzureResourceItemType.databaseServerContainer + } + }; + } + + private _databaseServerService: IAzureResourceDatabaseServerService = undefined; + private _apiWrapper: ApiWrapper = undefined; + private _extensionContext: ExtensionContext = undefined; + + private static readonly containerId = 'azure.resource.providers.databaseServer.treeDataProvider.databaseServerContainer'; + private static readonly containerLabel = localize('azure.resource.providers.databaseServer.treeDataProvider.databaseServerContainerLabel', 'SQL Servers'); +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/providers/databaseServer/interfaces.ts b/extensions/azurecore/src/azureResource/providers/databaseServer/interfaces.ts new file mode 100644 index 0000000000..7bfa3e5c21 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/databaseServer/interfaces.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { ServiceClientCredentials } from 'ms-rest'; + +import { azureResource } from '../../azure-resource'; +import { AzureResourceDatabaseServer } from './models'; + +export interface IAzureResourceDatabaseServerService { + getDatabaseServers(subscription: azureResource.AzureResourceSubscription, credentials: ServiceClientCredentials): Promise; +} + +export interface IAzureResourceDatabaseServerNode extends azureResource.IAzureResourceNode { + readonly databaseServer: AzureResourceDatabaseServer; +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/models.ts b/extensions/azurecore/src/azureResource/providers/databaseServer/models.ts similarity index 71% rename from extensions/azurecore/src/azureResource/models.ts rename to extensions/azurecore/src/azureResource/providers/databaseServer/models.ts index 21e5b4ab37..bf04e9acec 100644 --- a/extensions/azurecore/src/azureResource/models.ts +++ b/extensions/azurecore/src/azureResource/providers/databaseServer/models.ts @@ -5,21 +5,9 @@ 'use strict'; -export interface AzureResourceSubscription { - id: string; - name: string; -} - export interface AzureResourceDatabaseServer { name: string; fullName: string; loginName: string; defaultDatabaseName: string; -} - -export interface AzureResourceDatabase { - name: string; - serverName: string; - serverFullName: string; - loginName: string; -} +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/resourceService.ts b/extensions/azurecore/src/azureResource/resourceService.ts new file mode 100644 index 0000000000..3e8c4e83da --- /dev/null +++ b/extensions/azurecore/src/azureResource/resourceService.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { extensions, TreeItem } from 'vscode'; +import { Account } from 'sqlops'; + +import { azureResource } from './azure-resource'; +import { IAzureResourceNodeWithProviderId } from './interfaces'; + +export class AzureResourceService { + private constructor() { + } + + public static getInstance(): AzureResourceService { + return AzureResourceService._instance; + } + + public async listResourceProviderIds(): Promise { + await this.ensureResourceProvidersRegistered(); + + return Object.keys(this._resourceProviders); + } + + public registerResourceProvider(resourceProvider: azureResource.IAzureResourceProvider): void { + this.doRegisterResourceProvider(resourceProvider); + } + + public clearResourceProviders(): void { + this._resourceProviders = {}; + this._treeDataProviders = {}; + this._areResourceProvidersLoaded = false; + } + + public async getRootChildren(resourceProviderId: string, account: Account, subscription: azureResource.AzureResourceSubscription, tenatId: string): Promise { + await this.ensureResourceProvidersRegistered(); + + if (!(resourceProviderId in this._resourceProviders)) { + throw new Error(`Azure resource provider doesn't exist. Id: ${resourceProviderId}`); + } + + const treeDataProvider = this._treeDataProviders[resourceProviderId]; + const children = await treeDataProvider.getChildren(); + + return children.map((child) => { + resourceProviderId: resourceProviderId, + resourceNode: { + account: account, + subscription: subscription, + tenantId: tenatId, + treeItem: child.treeItem + } + }); + } + + public async getChildren(resourceProviderId: string, element: azureResource.IAzureResourceNode): Promise { + await this.ensureResourceProvidersRegistered(); + + if (!(resourceProviderId in this._resourceProviders)) { + throw new Error(`Azure resource provider doesn't exist. Id: ${resourceProviderId}`); + } + + const treeDataProvider = this._treeDataProviders[resourceProviderId]; + const children = await treeDataProvider.getChildren(element); + + return children.map((child) => { + resourceProviderId: resourceProviderId, + resourceNode: child + }); + } + + public async getTreeItem(resourceProviderId: string, element?: azureResource.IAzureResourceNode): Promise { + await this.ensureResourceProvidersRegistered(); + + if (!(resourceProviderId in this._resourceProviders)) { + throw new Error(`Azure resource provider doesn't exist. Id: ${resourceProviderId}`); + } + + const treeDataProvider = this._treeDataProviders[resourceProviderId]; + return treeDataProvider.getTreeItem(element); + } + + public get areResourceProvidersLoaded(): boolean { + return this._areResourceProvidersLoaded; + } + + public set areResourceProvidersLoaded(value: boolean) { + this._areResourceProvidersLoaded = value; + } + + private async ensureResourceProvidersRegistered(): Promise { + if (this._areResourceProvidersLoaded) { + return; + } + + for (const extension of extensions.all) { + const contributes = extension.packageJSON && extension.packageJSON.contributes; + if (!contributes) { + continue; + } + + if (contributes['hasAzureResourceProviders']) { + await extension.activate(); + + if (extension.exports && extension.exports.provideResources) { + for (const resourceProvider of extension.exports.provideResources()) { + this.doRegisterResourceProvider(resourceProvider); + } + } + } + } + + this._areResourceProvidersLoaded = true; + } + + private doRegisterResourceProvider(resourceProvider: azureResource.IAzureResourceProvider): void { + this._resourceProviders[resourceProvider.providerId] = resourceProvider; + this._treeDataProviders[resourceProvider.providerId] = resourceProvider.getTreeDataProvider(); + } + + private _areResourceProvidersLoaded: boolean = false; + private _resourceProviders: { [resourceProviderId: string]: azureResource.IAzureResourceProvider } = {}; + private _treeDataProviders: { [resourceProviderId: string]: azureResource.IAzureResourceTreeDataProvider } = {}; + + private static readonly _instance = new AzureResourceService(); +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/resourceTreeNode.ts b/extensions/azurecore/src/azureResource/resourceTreeNode.ts new file mode 100644 index 0000000000..707652b215 --- /dev/null +++ b/extensions/azurecore/src/azureResource/resourceTreeNode.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { NodeInfo } from 'sqlops'; +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { TreeNode } from './treeNode'; +import { AzureResourceService } from './resourceService'; +import { IAzureResourceNodeWithProviderId } from './interfaces'; +import { AzureResourceMessageTreeNode } from './messageTreeNode'; +import { AzureResourceErrorMessageUtil } from './utils'; + +export class AzureResourceResourceTreeNode extends TreeNode { + public constructor( + public readonly resourceNodeWithProviderId: IAzureResourceNodeWithProviderId, + parent: TreeNode + ) { + super(); + + this.parent = parent; + } + + public async getChildren(): Promise { + // It is a leaf node. + if (this.resourceNodeWithProviderId.resourceNode.treeItem.collapsibleState === TreeItemCollapsibleState.None) { + return []; + } + + try { + const children = await this._resourceService.getChildren(this.resourceNodeWithProviderId.resourceProviderId, this.resourceNodeWithProviderId.resourceNode); + + if (children.length === 0) { + return [AzureResourceMessageTreeNode.create(AzureResourceResourceTreeNode.noResourcesLabel, this)]; + } else { + return children.map((child) => { + // To make tree node's id unique, otherwise, treeModel.js would complain 'item already registered' + child.resourceNode.treeItem.id = `${this.resourceNodeWithProviderId.resourceNode.treeItem.id}.${child.resourceNode.treeItem.id}`; + return new AzureResourceResourceTreeNode(child, this); + }); + } + } catch (error) { + return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)]; + } + } + + public getTreeItem(): TreeItem | Promise { + return this._resourceService.getTreeItem(this.resourceNodeWithProviderId.resourceProviderId, this.resourceNodeWithProviderId.resourceNode); + } + + public getNodeInfo(): NodeInfo { + const treeItem = this.resourceNodeWithProviderId.resourceNode.treeItem; + + return { + label: treeItem.label, + isLeaf: treeItem.collapsibleState === TreeItemCollapsibleState.None ? true : false, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: treeItem.contextValue, + nodeSubType: undefined, + iconType: treeItem.contextValue + }; + } + + public get nodePathValue(): string { + return this.resourceNodeWithProviderId.resourceNode.treeItem.id; + } + + private _resourceService = AzureResourceService.getInstance(); + + private static readonly noResourcesLabel = localize('azure.resource.resourceTreeNode.noResourcesLabel', 'No Resources found.'); +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/servicePool.ts b/extensions/azurecore/src/azureResource/servicePool.ts deleted file mode 100644 index 27aff58d98..0000000000 --- a/extensions/azurecore/src/azureResource/servicePool.ts +++ /dev/null @@ -1,35 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { - IAzureResourceAccountService, - IAzureResourceCredentialService, - IAzureResourceSubscriptionService, - IAzureResourceSubscriptionFilterService, - IAzureResourceDatabaseService, - IAzureResourceDatabaseServerService, - IAzureResourceCacheService, - IAzureResourceContextService } from './interfaces'; - -export class AzureResourceServicePool { - private constructor() { } - - public static getInstance(): AzureResourceServicePool { - return AzureResourceServicePool._instance; - } - - public contextService: IAzureResourceContextService; - public cacheService: IAzureResourceCacheService; - public accountService: IAzureResourceAccountService; - public credentialService: IAzureResourceCredentialService; - public subscriptionService: IAzureResourceSubscriptionService; - public subscriptionFilterService: IAzureResourceSubscriptionFilterService; - public databaseService: IAzureResourceDatabaseService; - public databaseServerService: IAzureResourceDatabaseServerService; - - private static readonly _instance = new AzureResourceServicePool(); -} diff --git a/extensions/azurecore/src/azureResource/services/cacheService.ts b/extensions/azurecore/src/azureResource/services/cacheService.ts index ba0d100273..d6d4780470 100644 --- a/extensions/azurecore/src/azureResource/services/cacheService.ts +++ b/extensions/azurecore/src/azureResource/services/cacheService.ts @@ -5,21 +5,30 @@ 'use strict'; -import { ExtensionContext } from "vscode"; +import { ExtensionContext } from 'vscode'; -import { IAzureResourceCacheService } from "../interfaces"; +import { IAzureResourceCacheService } from '../interfaces'; export class AzureResourceCacheService implements IAzureResourceCacheService { public constructor( - public readonly context: ExtensionContext + context: ExtensionContext ) { + this._context = context; } - public get(key: string): T | undefined { - return this.context.workspaceState.get(key); + public generateKey(id: string): string { + return `${AzureResourceCacheService.cacheKeyPrefix}.${id}`; + } + + public get(key: string): T | undefined { + return this._context.workspaceState.get(key); } public update(key: string, value: T): void { - this.context.workspaceState.update(key, value); + this._context.workspaceState.update(key, value); } + + private _context: ExtensionContext = undefined; + + private static readonly cacheKeyPrefix = 'azure.resource.cache'; } \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/services/contextService.ts b/extensions/azurecore/src/azureResource/services/contextService.ts deleted file mode 100644 index 0e153d48e5..0000000000 --- a/extensions/azurecore/src/azureResource/services/contextService.ts +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { ExtensionContext } from "vscode"; -import { ApiWrapper } from "../../apiWrapper"; - -import { IAzureResourceContextService } from "../interfaces"; - -export class AzureResourceContextService implements IAzureResourceContextService { - public constructor( - context: ExtensionContext, - apiWrapper: ApiWrapper - ) { - this._context = context; - this._apiWrapper = apiWrapper; - } - - public getAbsolutePath(relativePath: string): string { - return this._context.asAbsolutePath(relativePath); - } - - public executeCommand(commandId: string, ...args: any[]): void { - this._apiWrapper.executeCommand(commandId, args); - } - - public showErrorMessage(errorMessage: string): void { - this._apiWrapper.showErrorMessage(errorMessage); - } - - private _context: ExtensionContext = undefined; - private _apiWrapper: ApiWrapper = undefined; -} diff --git a/extensions/azurecore/src/azureResource/services/credentialService.ts b/extensions/azurecore/src/azureResource/services/credentialService.ts deleted file mode 100644 index ff2ffc7858..0000000000 --- a/extensions/azurecore/src/azureResource/services/credentialService.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as sqlops from 'sqlops'; -import { TokenCredentials, ServiceClientCredentials } from 'ms-rest'; -import { ApiWrapper } from '../../apiWrapper'; -import * as nls from 'vscode-nls'; -const localize = nls.loadMessageBundle(); - -import { IAzureResourceCredentialService } from '../interfaces'; -import { AzureResourceCredentialError } from '../errors'; - -export class AzureResourceCredentialService implements IAzureResourceCredentialService { - public constructor( - apiWrapper: ApiWrapper - ) { - this._apiWrapper = apiWrapper; - } - - public async getCredentials(account: sqlops.Account, resource: sqlops.AzureResource): Promise { - try { - let credentials: TokenCredentials[] = []; - let tokens = await this._apiWrapper.getSecurityToken(account, resource); - - for (let tenant of account.properties.tenants) { - let token = tokens[tenant.id].token; - let tokenType = tokens[tenant.id].tokenType; - - credentials.push(new TokenCredentials(token, tokenType)); - } - - return credentials; - } catch (error) { - throw new AzureResourceCredentialError(localize('azureResource.services.credentialService.credentialError', 'Failed to get credential for account {0}. Please refresh the account.', account.key.accountId), error); - } - } - - private _apiWrapper: ApiWrapper = undefined; -} diff --git a/extensions/azurecore/src/azureResource/services/databaseServerService.ts b/extensions/azurecore/src/azureResource/services/databaseServerService.ts deleted file mode 100644 index 9145629a34..0000000000 --- a/extensions/azurecore/src/azureResource/services/databaseServerService.ts +++ /dev/null @@ -1,39 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { ServiceClientCredentials } from 'ms-rest'; -import { SqlManagementClient } from 'azure-arm-sql'; - -import { IAzureResourceDatabaseServerService } from '../interfaces'; -import { AzureResourceSubscription, AzureResourceDatabaseServer } from '../models'; - -export class AzureResourceDatabaseServerService implements IAzureResourceDatabaseServerService { - public async getDatabaseServers(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise { - let databaseServers: AzureResourceDatabaseServer[] = []; - for (let cred of credentials) { - let sqlManagementClient = new SqlManagementClient(cred, subscription.id); - try { - let svrs = await sqlManagementClient.servers.list(); - svrs.forEach((svr) => databaseServers.push({ - name: svr.name, - fullName: svr.fullyQualifiedDomainName, - loginName: svr.administratorLogin, - defaultDatabaseName: 'master' - })); - } catch (error) { - if (error.code === 'InvalidAuthenticationTokenTenant' && error.statusCode === 401) { - /** - * There may be multiple tenants for an account and it may throw exceptions like following. Just swallow the exception here. - * The access token is from the wrong issuer. It must match one of the tenants associated with this subscription. - */ - } - } - } - - return databaseServers; - } -} diff --git a/extensions/azurecore/src/azureResource/services/databaseService.ts b/extensions/azurecore/src/azureResource/services/databaseService.ts deleted file mode 100644 index 643db5c1e6..0000000000 --- a/extensions/azurecore/src/azureResource/services/databaseService.ts +++ /dev/null @@ -1,51 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { ServiceClientCredentials } from 'ms-rest'; -import { SqlManagementClient } from 'azure-arm-sql'; - -import { IAzureResourceDatabaseService } from '../interfaces'; -import { AzureResourceSubscription, AzureResourceDatabase } from '../models'; - -export class AzureResourceDatabaseService implements IAzureResourceDatabaseService { - public async getDatabases(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise { - let databases: AzureResourceDatabase[] = []; - for (let cred of credentials) { - let sqlManagementClient = new SqlManagementClient(cred, subscription.id); - try { - let svrs = await sqlManagementClient.servers.list(); - for (let svr of svrs) { - // Extract resource group name from svr.id - let svrIdRegExp = new RegExp(`\/subscriptions\/${subscription.id}\/resourceGroups\/(.+)\/providers\/Microsoft\.Sql\/servers\/${svr.name}`); - if (!svrIdRegExp.test(svr.id)) { - continue; - } - - let founds = svrIdRegExp.exec(svr.id); - let resouceGroup = founds[1]; - - let dbs = await sqlManagementClient.databases.listByServer(resouceGroup, svr.name); - dbs.forEach((db) => databases.push({ - name: db.name, - serverName: svr.name, - serverFullName: svr.fullyQualifiedDomainName, - loginName: svr.administratorLogin - })); - } - } catch (error) { - if (error.code === 'InvalidAuthenticationTokenTenant' && error.statusCode === 401) { - /** - * There may be multiple tenants for an account and it may throw exceptions like following. Just swallow the exception here. - * The access token is from the wrong issuer. It must match one of the tenants associated with this subscription. - */ - } - } - } - - return databases; - } -} diff --git a/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts b/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts index 9e6bea1b08..1429890096 100644 --- a/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts +++ b/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts @@ -8,11 +8,11 @@ import { WorkspaceConfiguration, ConfigurationTarget } from 'vscode'; import { Account } from 'sqlops'; +import { azureResource } from '../azure-resource'; import { IAzureResourceSubscriptionFilterService, IAzureResourceCacheService } from '../interfaces'; -import { AzureResourceSubscription } from '../models'; interface AzureResourceSelectedSubscriptionsCache { - selectedSubscriptions: { [accountId: string]: AzureResourceSubscription[]}; + selectedSubscriptions: { [accountId: string]: azureResource.AzureResourceSubscription[]}; } export class AzureResourceSubscriptionFilterService implements IAzureResourceSubscriptionFilterService { @@ -20,12 +20,14 @@ export class AzureResourceSubscriptionFilterService implements IAzureResourceSub cacheService: IAzureResourceCacheService ) { this._cacheService = cacheService; + + this._cacheKey = this._cacheService.generateKey('selectedSubscriptions'); } - public async getSelectedSubscriptions(account: Account): Promise { - let selectedSubscriptions: AzureResourceSubscription[] = []; + public async getSelectedSubscriptions(account: Account): Promise { + let selectedSubscriptions: azureResource.AzureResourceSubscription[] = []; - const cache = this._cacheService.get(AzureResourceSubscriptionFilterService.CacheKey); + const cache = this._cacheService.get(this._cacheKey); if (cache) { selectedSubscriptions = cache.selectedSubscriptions[account.key.accountId]; } @@ -33,10 +35,10 @@ export class AzureResourceSubscriptionFilterService implements IAzureResourceSub return selectedSubscriptions; } - public async saveSelectedSubscriptions(account: Account, selectedSubscriptions: AzureResourceSubscription[]): Promise { - let selectedSubscriptionsCache: { [accountId: string]: AzureResourceSubscription[]} = {}; + public async saveSelectedSubscriptions(account: Account, selectedSubscriptions: azureResource.AzureResourceSubscription[]): Promise { + let selectedSubscriptionsCache: { [accountId: string]: azureResource.AzureResourceSubscription[]} = {}; - const cache = this._cacheService.get(AzureResourceSubscriptionFilterService.CacheKey); + const cache = this._cacheService.get(this._cacheKey); if (cache) { selectedSubscriptionsCache = cache.selectedSubscriptions; } @@ -47,14 +49,14 @@ export class AzureResourceSubscriptionFilterService implements IAzureResourceSub selectedSubscriptionsCache[account.key.accountId] = selectedSubscriptions; - this._cacheService.update(AzureResourceSubscriptionFilterService.CacheKey, { selectedSubscriptions: selectedSubscriptionsCache }); + this._cacheService.update(this._cacheKey, { selectedSubscriptions: selectedSubscriptionsCache }); const filters: string[] = []; for (const accountId in selectedSubscriptionsCache) { filters.push(...selectedSubscriptionsCache[accountId].map((subcription) => `${accountId}/${subcription.id}/${subcription.name}`)); } - const resourceFilterConfig = this._config.inspect(AzureResourceSubscriptionFilterService.FilterConfigName); + const resourceFilterConfig = this._config.inspect(AzureResourceSubscriptionFilterService.filterConfigName); let configTarget = ConfigurationTarget.Global; if (resourceFilterConfig) { if (resourceFilterConfig.workspaceFolderValue) { @@ -66,12 +68,12 @@ export class AzureResourceSubscriptionFilterService implements IAzureResourceSub } } - await this._config.update(AzureResourceSubscriptionFilterService.FilterConfigName, filters, configTarget); + await this._config.update(AzureResourceSubscriptionFilterService.filterConfigName, filters, configTarget); } private _config: WorkspaceConfiguration = undefined; private _cacheService: IAzureResourceCacheService = undefined; + private _cacheKey: string = undefined; - private static readonly FilterConfigName = 'resourceFilter'; - private static readonly CacheKey = 'azureResource.cache.selectedSubscriptions'; + private static readonly filterConfigName = 'azure.resource.config.filter'; } diff --git a/extensions/azurecore/src/azureResource/services/subscriptionService.ts b/extensions/azurecore/src/azureResource/services/subscriptionService.ts index 9a8cfe0779..1fd6168289 100644 --- a/extensions/azurecore/src/azureResource/services/subscriptionService.ts +++ b/extensions/azurecore/src/azureResource/services/subscriptionService.ts @@ -9,24 +9,19 @@ import { Account } from 'sqlops'; import { ServiceClientCredentials } from 'ms-rest'; import { SubscriptionClient } from 'azure-arm-resource'; +import { azureResource } from '../azure-resource'; import { IAzureResourceSubscriptionService } from '../interfaces'; -import { AzureResourceSubscription } from '../models'; export class AzureResourceSubscriptionService implements IAzureResourceSubscriptionService { - public async getSubscriptions(account: Account, credentials: ServiceClientCredentials[]): Promise { - let subscriptions: AzureResourceSubscription[] = []; - for (let cred of credentials) { - let subClient = new SubscriptionClient.SubscriptionClient(cred); - try { - let subs = await subClient.subscriptions.list(); - subs.forEach((sub) => subscriptions.push({ - id: sub.subscriptionId, - name: sub.displayName - })); - } catch (error) { - // Swallow the exception here. - } - } + public async getSubscriptions(account: Account, credential: ServiceClientCredentials): Promise { + const subscriptions: azureResource.AzureResourceSubscription[] = []; + + const subClient = new SubscriptionClient.SubscriptionClient(credential); + const subs = await subClient.subscriptions.list(); + subs.forEach((sub) => subscriptions.push({ + id: sub.subscriptionId, + name: sub.displayName + })); return subscriptions; } diff --git a/extensions/azurecore/src/azureResource/services/tenantService.ts b/extensions/azurecore/src/azureResource/services/tenantService.ts new file mode 100644 index 0000000000..42905a6c8e --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/tenantService.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as request from 'request'; + +import { azureResource } from '../azure-resource'; +import { IAzureResourceTenantService } from '../interfaces'; + +export class AzureResourceTenantService implements IAzureResourceTenantService { + public async getTenantId(subscription: azureResource.AzureResourceSubscription): Promise { + const requestPromisified = new Promise((resolve, reject) => { + const url = `https://management.azure.com/subscriptions/${subscription.id}?api-version=2014-04-01`; + request(url, function (error, response, body) { + if (response.statusCode === 401) { + const tenantIdRegEx = /authorization_uri="https:\/\/login\.windows\.net\/([0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12})"/; + const teantIdString = response.headers['www-authenticate']; + if (tenantIdRegEx.test(teantIdString)) { + resolve(tenantIdRegEx.exec(teantIdString)[1]); + } else { + reject(); + } + } + }); + }); + + return await requestPromisified; + } +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts b/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts index 47008428aa..24e760166d 100644 --- a/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts +++ b/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts @@ -7,10 +7,10 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { NodeInfo } from 'sqlops'; -import { TreeNode } from '../../treeNodes'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); +import { TreeNode } from '../treeNode'; import { AzureResourceItemType } from '../constants'; export class AzureResourceAccountNotSignedInTreeNode extends TreeNode { @@ -19,11 +19,11 @@ export class AzureResourceAccountNotSignedInTreeNode extends TreeNode { } public getTreeItem(): TreeItem | Promise { - let item = new TreeItem(AzureResourceAccountNotSignedInTreeNode.SignInLabel, TreeItemCollapsibleState.None); + let item = new TreeItem(AzureResourceAccountNotSignedInTreeNode.signInLabel, TreeItemCollapsibleState.None); item.contextValue = AzureResourceItemType.message; item.command = { - title: AzureResourceAccountNotSignedInTreeNode.SignInLabel, - command: 'azureresource.signin', + title: AzureResourceAccountNotSignedInTreeNode.signInLabel, + command: 'azure.resource.signin', arguments: [this] }; return item; @@ -31,7 +31,7 @@ export class AzureResourceAccountNotSignedInTreeNode extends TreeNode { public getNodeInfo(): NodeInfo { return { - label: AzureResourceAccountNotSignedInTreeNode.SignInLabel, + label: AzureResourceAccountNotSignedInTreeNode.signInLabel, isLeaf: true, errorMessage: undefined, metadata: undefined, @@ -47,5 +47,5 @@ export class AzureResourceAccountNotSignedInTreeNode extends TreeNode { return 'message_accountNotSignedIn'; } - private static readonly SignInLabel = localize('azureResource.tree.accountNotSignedInTreeNode.signIn', 'Sign in to Azure ...'); + private static readonly signInLabel = localize('azure.resource.tree.accountNotSignedInTreeNode.signInLabel', 'Sign in to Azure ...'); } diff --git a/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts b/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts index a95a485ece..aa9ba624a1 100644 --- a/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts +++ b/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts @@ -6,44 +6,59 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { Account, NodeInfo } from 'sqlops'; -import { TreeNode } from '../../treeNodes'; +import { Account, NodeInfo, AzureResource } from 'sqlops'; +import { TokenCredentials } from 'ms-rest'; +import { AppContext } from '../../appContext'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); +import { azureResource } from '../azure-resource'; +import { TreeNode } from '../treeNode'; +import { AzureResourceCredentialError } from '../errors'; import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; -import { AzureResourceItemType } from '../constants'; +import { AzureResourceItemType, AzureResourceServiceNames } from '../constants'; import { AzureResourceSubscriptionTreeNode } from './subscriptionTreeNode'; -import { AzureResourceMessageTreeNode } from './messageTreeNode'; +import { AzureResourceMessageTreeNode } from '../messageTreeNode'; import { AzureResourceErrorMessageUtil } from '../utils'; -import { AzureResourceSubscription } from '../models'; -import { IAzureResourceTreeChangeHandler } from './treeProvider'; +import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; +import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureResourceTenantService } from '../../azureResource/interfaces'; export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNodeBase { public constructor( - account: Account, + public readonly account: Account, + appContext: AppContext, treeChangeHandler: IAzureResourceTreeChangeHandler ) { - super(account, treeChangeHandler, undefined); + super(appContext, treeChangeHandler, undefined); + + this._subscriptionService = this.appContext.getService(AzureResourceServiceNames.subscriptionService); + this._subscriptionFilterService = this.appContext.getService(AzureResourceServiceNames.subscriptionFilterService); + this._tenantService = this.appContext.getService(AzureResourceServiceNames.tenantService); this._id = `account_${this.account.key.accountId}`; + this.setCacheKey(`${this._id}.subscriptions`); this._label = this.generateLabel(); } public async getChildren(): Promise { try { - let subscriptions: AzureResourceSubscription[] = []; + let subscriptions: azureResource.AzureResourceSubscription[] = []; if (this._isClearingCache) { - const credentials = await this.getCredentials(); - subscriptions = (await this.servicePool.subscriptionService.getSubscriptions(this.account, credentials)) || []; + try { + const tokens = await this.appContext.apiWrapper.getSecurityToken(this.account, AzureResource.ResourceManagement); - let cache = this.getCache(); - if (!cache) { - cache = { subscriptions: { } }; + for (const tenant of this.account.properties.tenants) { + const token = tokens[tenant.id].token; + const tokenType = tokens[tenant.id].tokenType; + + subscriptions.push(...(await this._subscriptionService.getSubscriptions(this.account, new TokenCredentials(token, tokenType)) || [])); + } + } catch (error) { + throw new AzureResourceCredentialError(localize('azure.resource.tree.accountTreeNode.credentialError', 'Failed to get credential for account {0}. Please refresh the account.', this.account.key.accountId), error); } - cache.subscriptions[this.account.key.accountId] = subscriptions; - this.updateCache(cache); + + this.updateCache(subscriptions); this._isClearingCache = false; } else { @@ -52,8 +67,8 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode this._totalSubscriptionCount = subscriptions.length; - let selectedSubscriptions = await this.servicePool.subscriptionFilterService.getSelectedSubscriptions(this.account); - let selectedSubscriptionIds = (selectedSubscriptions || []).map((subscription) => subscription.id); + const selectedSubscriptions = await this._subscriptionFilterService.getSelectedSubscriptions(this.account); + const selectedSubscriptionIds = (selectedSubscriptions || []).map((subscription) => subscription.id); if (selectedSubscriptionIds.length > 0) { subscriptions = subscriptions.filter((subscription) => selectedSubscriptionIds.indexOf(subscription.id) !== -1); this._selectedSubscriptionCount = selectedSubscriptionIds.length; @@ -65,31 +80,36 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode this.refreshLabel(); if (subscriptions.length === 0) { - return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.NoSubscriptions, this)]; + return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.noSubscriptionsLabel, this)]; } else { - return subscriptions.map((subscription) => new AzureResourceSubscriptionTreeNode(subscription, this.account, this.treeChangeHandler, this)); + return await Promise.all(subscriptions.map(async (subscription) => { + const tenantId = await this._tenantService.getTenantId(subscription); + + return new AzureResourceSubscriptionTreeNode(this.account, subscription, tenantId, this.appContext, this.treeChangeHandler, this); + })); } } catch (error) { - return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)]; + if (error instanceof AzureResourceCredentialError) { + this.appContext.apiWrapper.showErrorMessage(error.message); + + this.appContext.apiWrapper.executeCommand('azure.resource.signin'); + } else { + return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)]; + } } } - public async getCachedSubscriptions(): Promise { - const subscriptions: AzureResourceSubscription[] = []; - const cache = this.getCache(); - if (cache) { - subscriptions.push(...cache.subscriptions[this.account.key.accountId]); - } - return subscriptions; + public async getCachedSubscriptions(): Promise { + return this.getCache(); } public getTreeItem(): TreeItem | Promise { - let item = new TreeItem(this._label, TreeItemCollapsibleState.Collapsed); + const item = new TreeItem(this._label, TreeItemCollapsibleState.Collapsed); item.id = this._id; item.contextValue = AzureResourceItemType.account; item.iconPath = { - dark: this.servicePool.contextService.getAbsolutePath('resources/dark/account_inverse.svg'), - light: this.servicePool.contextService.getAbsolutePath('resources/light/account.svg') + dark: this.appContext.extensionContext.asAbsolutePath('resources/dark/account_inverse.svg'), + light: this.appContext.extensionContext.asAbsolutePath('resources/light/account.svg') }; return item; } @@ -128,10 +148,6 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode } } - protected get cacheKey(): string { - return 'azureResource.cache.subscriptions'; - } - private generateLabel(): string { let label = `${this.account.displayInfo.displayName} (${this.account.key.accountId})`; @@ -142,14 +158,14 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode return label; } + private _subscriptionService: IAzureResourceSubscriptionService = undefined; + private _subscriptionFilterService: IAzureResourceSubscriptionFilterService = undefined; + private _tenantService: IAzureResourceTenantService = undefined; + private _id: string = undefined; private _label: string = undefined; private _totalSubscriptionCount = 0; private _selectedSubscriptionCount = 0; - private static readonly NoSubscriptions = localize('azureResource.tree.accountTreeNode.noSubscriptions', 'No Subscriptions found.'); -} - -interface AzureResourceSubscriptionsCache { - subscriptions: { [accountId: string]: AzureResourceSubscription[] }; -} + private static readonly noSubscriptionsLabel = localize('azure.resource.tree.accountTreeNode.noSubscriptionsLabel', 'No Subscriptions found.'); +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts b/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts index a7e1ab2b19..35530fcd01 100644 --- a/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts +++ b/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts @@ -5,16 +5,16 @@ 'use strict'; -import * as sqlops from 'sqlops'; -import { ServiceClientCredentials } from 'ms-rest'; -import { TreeNode } from '../../treeNodes'; +import { AppContext } from '../../appContext'; -import { AzureResourceServicePool } from '../servicePool'; -import { AzureResourceCredentialError } from '../errors'; +import { TreeNode } from '../treeNode'; import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; +import { IAzureResourceCacheService } from '../../azureResource/interfaces'; +import { AzureResourceServiceNames } from '../constants'; export abstract class AzureResourceTreeNodeBase extends TreeNode { public constructor( + public readonly appContext: AppContext, public readonly treeChangeHandler: IAzureResourceTreeChangeHandler, parent: TreeNode ) { @@ -22,17 +22,17 @@ export abstract class AzureResourceTreeNodeBase extends TreeNode { this.parent = parent; } - - public readonly servicePool = AzureResourceServicePool.getInstance(); } export abstract class AzureResourceContainerTreeNodeBase extends AzureResourceTreeNodeBase { public constructor( - public readonly account: sqlops.Account, + appContext: AppContext, treeChangeHandler: IAzureResourceTreeChangeHandler, parent: TreeNode ) { - super(treeChangeHandler, parent); + super(appContext, treeChangeHandler, parent); + + this._cacheService = this.appContext.getService(AzureResourceServiceNames.cacheService); } public clearCache(): void { @@ -43,29 +43,19 @@ export abstract class AzureResourceContainerTreeNodeBase extends AzureResourceTr return this._isClearingCache; } - protected async getCredentials(): Promise { - try { - return await this.servicePool.credentialService.getCredentials(this.account, sqlops.AzureResource.ResourceManagement); - } catch (error) { - if (error instanceof AzureResourceCredentialError) { - this.servicePool.contextService.showErrorMessage(error.message); - - this.servicePool.contextService.executeCommand('azureresource.signin'); - } else { - throw error; - } - } - } + protected setCacheKey(id: string): void { + this._cacheKey = this._cacheService.generateKey(id); + } protected updateCache(cache: T): void { - this.servicePool.cacheService.update(this.cacheKey, cache); + this._cacheService.update(this._cacheKey, cache); } protected getCache(): T { - return this.servicePool.cacheService.get(this.cacheKey); + return this._cacheService.get(this._cacheKey); } - protected abstract get cacheKey(): string; - protected _isClearingCache = true; + private _cacheService: IAzureResourceCacheService = undefined; + private _cacheKey: string = undefined; } diff --git a/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts deleted file mode 100644 index 26d5f6d70c..0000000000 --- a/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts +++ /dev/null @@ -1,103 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { Account, NodeInfo } from 'sqlops'; -import { TreeNode } from '../../treeNodes'; -import * as nls from 'vscode-nls'; -const localize = nls.loadMessageBundle(); - -import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; -import { AzureResourceItemType } from '../constants'; -import { AzureResourceErrorMessageUtil } from '../utils'; -import { AzureResourceDatabaseTreeNode } from './databaseTreeNode'; -import { AzureResourceMessageTreeNode } from './messageTreeNode'; -import { AzureResourceSubscription, AzureResourceDatabase } from '../models'; -import { IAzureResourceTreeChangeHandler } from './treeProvider'; - -export class AzureResourceDatabaseContainerTreeNode extends AzureResourceContainerTreeNodeBase { - public constructor( - public readonly subscription: AzureResourceSubscription, - account: Account, - treeChangeHandler: IAzureResourceTreeChangeHandler, - parent: TreeNode - ) { - super(account, treeChangeHandler, parent); - } - - public async getChildren(): Promise { - try { - let databases: AzureResourceDatabase[] = []; - - if (this._isClearingCache) { - let credentials = await this.getCredentials(); - databases = (await this.servicePool.databaseService.getDatabases(this.subscription, credentials)) || []; - - let cache = this.getCache(); - if (!cache) { - cache = { databases: { } }; - } - cache.databases[this.subscription.id] = databases; - this.updateCache(cache); - - this._isClearingCache = false; - } else { - const cache = this.getCache(); - if (cache) { - databases = cache.databases[this.subscription.id] || []; - } - } - - if (databases.length === 0) { - return [AzureResourceMessageTreeNode.create(AzureResourceDatabaseContainerTreeNode.NoDatabases, this)]; - } else { - return databases.map((database) => new AzureResourceDatabaseTreeNode(database, this.treeChangeHandler, this)); - } - } catch (error) { - return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)]; - } - } - - public getTreeItem(): TreeItem | Promise { - let item = new TreeItem(AzureResourceDatabaseContainerTreeNode.Label, TreeItemCollapsibleState.Collapsed); - item.contextValue = AzureResourceItemType.databaseContainer; - item.iconPath = { - dark: this.servicePool.contextService.getAbsolutePath('resources/dark/folder_inverse.svg'), - light: this.servicePool.contextService.getAbsolutePath('resources/light/folder.svg') - }; - return item; - } - - public getNodeInfo(): NodeInfo { - return { - label: AzureResourceDatabaseContainerTreeNode.Label, - isLeaf: false, - errorMessage: undefined, - metadata: undefined, - nodePath: this.generateNodePath(), - nodeStatus: undefined, - nodeType: AzureResourceItemType.databaseContainer, - nodeSubType: undefined, - iconType: AzureResourceItemType.databaseContainer - }; - } - - public get nodePathValue(): string { - return 'databaseContainer'; - } - - protected get cacheKey(): string { - return 'azureResource.cache.databases'; - } - - private static readonly Label = localize('azureResource.tree.databaseContainerTreeNode.label', 'SQL Databases'); - private static readonly NoDatabases = localize('azureResource.tree.databaseContainerTreeNode.noDatabases', 'No SQL Databases found.'); -} - -interface AzureResourceDatabasesCache { - databases: { [subscriptionId: string]: AzureResourceDatabase[] }; -} diff --git a/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts deleted file mode 100644 index 6f2921e092..0000000000 --- a/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts +++ /dev/null @@ -1,103 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { Account, NodeInfo } from 'sqlops'; -import { TreeNode } from '../../treeNodes'; -import * as nls from 'vscode-nls'; -const localize = nls.loadMessageBundle(); - -import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; -import { AzureResourceItemType } from '../constants'; -import { AzureResourceMessageTreeNode } from './messageTreeNode'; -import { AzureResourceErrorMessageUtil } from '../utils'; -import { AzureResourceSubscription, AzureResourceDatabaseServer } from '../models'; -import { AzureResourceDatabaseServerTreeNode } from './databaseServerTreeNode'; -import { IAzureResourceTreeChangeHandler } from './treeProvider'; - -export class AzureResourceDatabaseServerContainerTreeNode extends AzureResourceContainerTreeNodeBase { - public constructor( - public readonly subscription: AzureResourceSubscription, - account: Account, - treeChangeHandler: IAzureResourceTreeChangeHandler, - parent: TreeNode - ) { - super(account, treeChangeHandler, parent); - } - - public async getChildren(): Promise { - try { - let databaseServers: AzureResourceDatabaseServer[] = []; - - if (this._isClearingCache) { - let credentials = await this.getCredentials(); - databaseServers = (await this.servicePool.databaseServerService.getDatabaseServers(this.subscription, credentials)) || []; - - let cache = this.getCache(); - if (!cache) { - cache = { databaseServers: { } }; - } - cache.databaseServers[this.subscription.id] = databaseServers; - this.updateCache(cache); - - this._isClearingCache = false; - } else { - const cache = this.getCache(); - if (cache) { - databaseServers = cache.databaseServers[this.subscription.id] || []; - } - } - - if (databaseServers.length === 0) { - return [AzureResourceMessageTreeNode.create(AzureResourceDatabaseServerContainerTreeNode.NoDatabaseServers, this)]; - } else { - return databaseServers.map((server) => new AzureResourceDatabaseServerTreeNode(server, this.treeChangeHandler, this)); - } - } catch (error) { - return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)]; - } - } - - public getTreeItem(): TreeItem | Promise { - let item = new TreeItem(AzureResourceDatabaseServerContainerTreeNode.Label, TreeItemCollapsibleState.Collapsed); - item.contextValue = AzureResourceItemType.databaseServerContainer; - item.iconPath = { - dark: this.servicePool.contextService.getAbsolutePath('resources/dark/folder_inverse.svg'), - light: this.servicePool.contextService.getAbsolutePath('resources/light/folder.svg') - }; - return item; - } - - public getNodeInfo(): NodeInfo { - return { - label: AzureResourceDatabaseServerContainerTreeNode.Label, - isLeaf: false, - errorMessage: undefined, - metadata: undefined, - nodePath: this.generateNodePath(), - nodeStatus: undefined, - nodeType: AzureResourceItemType.databaseServerContainer, - nodeSubType: undefined, - iconType: AzureResourceItemType.databaseServerContainer - }; - } - - public get nodePathValue(): string { - return 'databaseServerContainer'; - } - - protected get cacheKey(): string { - return 'azureResource.cache.databaseServers'; - } - - private static readonly Label = localize('azureResource.tree.databaseServerContainerTreeNode.label', 'SQL Servers'); - private static readonly NoDatabaseServers = localize('azureResource.tree.databaseContainerTreeNode.noDatabaseServers', 'No SQL Servers found.'); -} - -interface AzureResourceDatabaseServersCache { - databaseServers: { [subscriptionId: string]: AzureResourceDatabaseServer[] }; -} diff --git a/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts deleted file mode 100644 index 2c8cf5bd4b..0000000000 --- a/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { NodeInfo } from 'sqlops'; -import { TreeNode } from '../../treeNodes'; - -import { AzureResourceTreeNodeBase } from './baseTreeNodes'; -import { AzureResourceItemType } from '../constants'; -import { AzureResourceDatabaseServer } from '../models'; -import { IAzureResourceTreeChangeHandler } from './treeProvider'; - -export class AzureResourceDatabaseServerTreeNode extends AzureResourceTreeNodeBase { - public constructor( - public readonly databaseServer: AzureResourceDatabaseServer, - treeChangeHandler: IAzureResourceTreeChangeHandler, - parent: TreeNode - ) { - super(treeChangeHandler, parent); - } - - public async getChildren(): Promise { - return []; - } - - public getTreeItem(): TreeItem | Promise { - let item = new TreeItem(this.databaseServer.name, TreeItemCollapsibleState.None); - item.contextValue = AzureResourceItemType.databaseServer; - item.iconPath = { - dark: this.servicePool.contextService.getAbsolutePath('resources/dark/sql_server_inverse.svg'), - light: this.servicePool.contextService.getAbsolutePath('resources/light/sql_server.svg') - }; - return item; - } - - public getNodeInfo(): NodeInfo { - return { - label: this.databaseServer.name, - isLeaf: true, - errorMessage: undefined, - metadata: undefined, - nodePath: this.generateNodePath(), - nodeStatus: undefined, - nodeType: AzureResourceItemType.databaseServer, - nodeSubType: undefined, - iconType: AzureResourceItemType.databaseServer - }; - } - - public get nodePathValue(): string { - return `databaseServer_${this.databaseServer.name}`; - } -} diff --git a/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts deleted file mode 100644 index 1089a6e0c5..0000000000 --- a/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts +++ /dev/null @@ -1,61 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { NodeInfo } from 'sqlops'; -import { TreeNode } from '../../treeNodes'; - -import { AzureResourceTreeNodeBase } from './baseTreeNodes'; -import { AzureResourceItemType } from '../constants'; -import { AzureResourceDatabase } from '../models'; -import { IAzureResourceTreeChangeHandler } from './treeProvider'; - -export class AzureResourceDatabaseTreeNode extends AzureResourceTreeNodeBase { - public constructor( - public readonly database: AzureResourceDatabase, - treeChangeHandler: IAzureResourceTreeChangeHandler, - parent: TreeNode - ) { - super(treeChangeHandler, parent); - - this._label = `${this.database.name} (${this.database.serverName})`; - } - - public async getChildren(): Promise { - return []; - } - - public getTreeItem(): TreeItem | Promise { - let item = new TreeItem(this._label, TreeItemCollapsibleState.None); - item.contextValue = AzureResourceItemType.database; - item.iconPath = { - dark: this.servicePool.contextService.getAbsolutePath('resources/dark/sql_database_inverse.svg'), - light: this.servicePool.contextService.getAbsolutePath('resources/light/sql_database.svg') - }; - return item; - } - - public getNodeInfo(): NodeInfo { - return { - label: this._label, - isLeaf: true, - errorMessage: undefined, - metadata: undefined, - nodePath: this.generateNodePath(), - nodeStatus: undefined, - nodeType: AzureResourceItemType.database, - nodeSubType: undefined, - iconType: AzureResourceItemType.database - }; - } - - public get nodePathValue(): string { - return `database_${this.database.name}`; - } - - private _label: string = undefined; -} diff --git a/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts b/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts index ff8474ca3f..8765dafb58 100644 --- a/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts +++ b/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts @@ -7,38 +7,66 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Account, NodeInfo } from 'sqlops'; -import { TreeNode } from '../../treeNodes'; +import { AppContext } from '../../appContext'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); -import { AzureResourceTreeNodeBase, AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; +import { azureResource } from '../azure-resource'; +import { TreeNode } from '../treeNode'; +import { IAzureResourceNodeWithProviderId } from '../interfaces'; +import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; import { AzureResourceItemType } from '../constants'; -import { AzureResourceDatabaseContainerTreeNode } from './databaseContainerTreeNode'; -import { AzureResourceDatabaseServerContainerTreeNode } from './databaseServerContainerTreeNode'; -import { AzureResourceSubscription } from '../models'; import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; +import { AzureResourceMessageTreeNode } from '../messageTreeNode'; +import { AzureResourceErrorMessageUtil } from '../utils'; +import { AzureResourceService } from '../resourceService'; +import { AzureResourceResourceTreeNode } from '../resourceTreeNode'; -export class AzureResourceSubscriptionTreeNode extends AzureResourceTreeNodeBase { +export class AzureResourceSubscriptionTreeNode extends AzureResourceContainerTreeNodeBase { public constructor( - public readonly subscription: AzureResourceSubscription, - account: Account, + public readonly account: Account, + public readonly subscription: azureResource.AzureResourceSubscription, + public readonly tenatId: string, + appContext: AppContext, treeChangeHandler: IAzureResourceTreeChangeHandler, parent: TreeNode ) { - super(treeChangeHandler, parent); + super(appContext, treeChangeHandler, parent); - this._children.push(new AzureResourceDatabaseContainerTreeNode(subscription, account, treeChangeHandler, this)); - this._children.push(new AzureResourceDatabaseServerContainerTreeNode(subscription, account, treeChangeHandler, this)); + this._id = `account_${this.account.key.accountId}.subscription_${this.subscription.id}.tenant_${this.tenatId}`; + this.setCacheKey(`${this._id}.resources`); } public async getChildren(): Promise { - return this._children; + try { + const resourceService = AzureResourceService.getInstance(); + + const children: IAzureResourceNodeWithProviderId[] = []; + + for (const resourceProviderId of await resourceService.listResourceProviderIds()) { + children.push(...await resourceService.getRootChildren(resourceProviderId, this.account, this.subscription, this.tenatId)); + } + + if (children.length === 0) { + return [AzureResourceMessageTreeNode.create(AzureResourceSubscriptionTreeNode.noResourcesLabel, this)]; + } else { + return children.map((child) => { + // To make tree node's id unique, otherwise, treeModel.js would complain 'item already registered' + child.resourceNode.treeItem.id = `${this._id}.${child.resourceNode.treeItem.id}`; + return new AzureResourceResourceTreeNode(child, this); + }); + } + } catch (error) { + return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)]; + } } public getTreeItem(): TreeItem | Promise { - let item = new TreeItem(this.subscription.name, TreeItemCollapsibleState.Collapsed); + const item = new TreeItem(this.subscription.name, TreeItemCollapsibleState.Collapsed); item.contextValue = AzureResourceItemType.subscription; item.iconPath = { - dark: this.servicePool.contextService.getAbsolutePath('resources/dark/subscription_inverse.svg'), - light: this.servicePool.contextService.getAbsolutePath('resources/light/subscription.svg') + dark: this.appContext.extensionContext.asAbsolutePath('resources/dark/subscription_inverse.svg'), + light: this.appContext.extensionContext.asAbsolutePath('resources/light/subscription.svg') }; return item; } @@ -58,8 +86,10 @@ export class AzureResourceSubscriptionTreeNode extends AzureResourceTreeNodeBase } public get nodePathValue(): string { - return `subscription_${this.subscription.id}`; + return this._id; } - private _children: AzureResourceContainerTreeNodeBase[] = []; + private _id: string = undefined; + + private static readonly noResourcesLabel = localize('azure.resource.tree.subscriptionTreeNode.noResourcesLabel', 'No Resources found.'); } diff --git a/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts b/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts index b7db2ed6b4..f1648cbf22 100644 --- a/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts +++ b/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts @@ -5,7 +5,7 @@ 'use strict'; -import { TreeNode } from '../../treeNodes'; +import { TreeNode } from '../treeNode'; export interface IAzureResourceTreeChangeHandler { notifyNodeChanged(node: TreeNode): void; diff --git a/extensions/azurecore/src/azureResource/tree/treeProvider.ts b/extensions/azurecore/src/azureResource/tree/treeProvider.ts index 7cfd4ce89f..7e47191c39 100644 --- a/extensions/azurecore/src/azureResource/tree/treeProvider.ts +++ b/extensions/azurecore/src/azureResource/tree/treeProvider.ts @@ -6,26 +6,25 @@ 'use strict'; import { TreeDataProvider, EventEmitter, Event, TreeItem } from 'vscode'; -import { DidChangeAccountsParams } from 'sqlops'; -import { TreeNode } from '../../treeNodes'; import { setInterval, clearInterval } from 'timers'; +import { AppContext } from '../../appContext'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -import { AzureResourceServicePool } from '../servicePool'; +import { TreeNode } from '../treeNode'; import { AzureResourceAccountTreeNode } from './accountTreeNode'; import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode'; -import { AzureResourceMessageTreeNode } from './messageTreeNode'; -import { AzureResourceContainerTreeNodeBase, AzureResourceTreeNodeBase } from './baseTreeNodes'; +import { AzureResourceMessageTreeNode } from '../messageTreeNode'; +import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; import { AzureResourceErrorMessageUtil } from '../utils'; - -export interface IAzureResourceTreeChangeHandler { - notifyNodeChanged(node: TreeNode): void; -} +import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; +import { IAzureResourceAccountService } from '../../azureResource/interfaces'; +import { AzureResourceServiceNames } from '../constants'; export class AzureResourceTreeProvider implements TreeDataProvider, IAzureResourceTreeChangeHandler { - public constructor() { - AzureResourceServicePool.getInstance().accountService.onDidChangeAccounts((e: DidChangeAccountsParams) => { this._onDidChangeTreeData.fire(undefined); }); + public constructor( + public readonly appContext: AppContext + ) { } public async getChildren(element?: TreeNode): Promise { @@ -37,7 +36,7 @@ export class AzureResourceTreeProvider implements TreeDataProvider, IA this._loadingTimer = setInterval(async () => { try { // Call sqlops.accounts.getAllAccounts() to determine whether the system has been initialized. - await AzureResourceServicePool.getInstance().accountService.getAccounts(); + await this.appContext.getService(AzureResourceServiceNames.accountService).getAccounts(); // System has been initialized this.isSystemInitialized = true; @@ -51,16 +50,16 @@ export class AzureResourceTreeProvider implements TreeDataProvider, IA // System not initialized yet this.isSystemInitialized = false; } - }, AzureResourceTreeProvider.LoadingTimerInterval); + }, AzureResourceTreeProvider.loadingTimerInterval); - return [AzureResourceMessageTreeNode.create(AzureResourceTreeProvider.Loading, undefined)]; + return [AzureResourceMessageTreeNode.create(AzureResourceTreeProvider.loadingLabel, undefined)]; } try { - const accounts = await AzureResourceServicePool.getInstance().accountService.getAccounts(); + const accounts = await this.appContext.getService(AzureResourceServiceNames.accountService).getAccounts(); if (accounts && accounts.length > 0) { - return accounts.map((account) => new AzureResourceAccountTreeNode(account, this)); + return accounts.map((account) => new AzureResourceAccountTreeNode(account, this.appContext, this)); } else { return [new AzureResourceAccountNotSignedInTreeNode()]; } @@ -96,6 +95,6 @@ export class AzureResourceTreeProvider implements TreeDataProvider, IA private _loadingTimer: NodeJS.Timer = undefined; private _onDidChangeTreeData = new EventEmitter(); - private static readonly Loading = localize('azureResource.tree.treeProvider.loading', 'Loading ...'); - private static readonly LoadingTimerInterval = 5000; + private static readonly loadingLabel = localize('azure.resource.tree.treeProvider.loadingLabel', 'Loading ...'); + private static readonly loadingTimerInterval = 5000; } diff --git a/extensions/azurecore/src/treeNodes.ts b/extensions/azurecore/src/azureResource/treeNode.ts similarity index 90% rename from extensions/azurecore/src/treeNodes.ts rename to extensions/azurecore/src/azureResource/treeNode.ts index 7136ba98bd..72a0ea772c 100644 --- a/extensions/azurecore/src/treeNodes.ts +++ b/extensions/azurecore/src/azureResource/treeNode.ts @@ -11,16 +11,6 @@ import * as vscode from 'vscode'; type TreeNodePredicate = (node: TreeNode) => boolean; export abstract class TreeNode { - private _parent: TreeNode = undefined; - - public get parent(): TreeNode { - return this._parent; - } - - public set parent(node: TreeNode) { - this._parent = node; - } - public generateNodePath(): string { let path = undefined; if (this.parent) { @@ -65,13 +55,23 @@ export abstract class TreeNode { return undefined; } + public get parent(): TreeNode { + return this._parent; + } + + public set parent(node: TreeNode) { + this._parent = node; + } + + public abstract getChildren(refreshChildren: boolean): TreeNode[] | Promise; + public abstract getTreeItem(): vscode.TreeItem | Promise; + + public abstract getNodeInfo(): sqlops.NodeInfo; + /** * The value to use for this node in the node path */ public abstract get nodePathValue(): string; - abstract getChildren(refreshChildren: boolean): TreeNode[] | Promise; - abstract getTreeItem(): vscode.TreeItem | Promise; - - abstract getNodeInfo(): sqlops.NodeInfo; + private _parent: TreeNode = undefined; } diff --git a/extensions/azurecore/src/azureResource/utils.ts b/extensions/azurecore/src/azureResource/utils.ts index 99f177eafb..a3c7cedbc0 100644 --- a/extensions/azurecore/src/azureResource/utils.ts +++ b/extensions/azurecore/src/azureResource/utils.ts @@ -12,10 +12,9 @@ export function getErrorMessage(error: Error | string): string { return (error instanceof Error) ? error.message : error; } - export class AzureResourceErrorMessageUtil { public static getErrorMessage(error: Error | string): string { - return localize('azureResource.error', 'Error: {0}', getErrorMessage(error)); + return localize('azure.resource.error', 'Error: {0}', getErrorMessage(error)); } } diff --git a/extensions/azurecore/src/controllers/azureResourceController.ts b/extensions/azurecore/src/controllers/azureResourceController.ts new file mode 100644 index 0000000000..8778d29908 --- /dev/null +++ b/extensions/azurecore/src/controllers/azureResourceController.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import ControllerBase from './controllerBase'; +import { DidChangeAccountsParams } from 'sqlops'; + +import { + IAzureResourceCacheService, + IAzureResourceAccountService, + IAzureResourceSubscriptionService, + IAzureResourceSubscriptionFilterService, + IAzureResourceTenantService } from '../azureResource/interfaces'; +import { AzureResourceServiceNames } from '../azureResource/constants'; +import { AzureResourceTreeProvider } from '../azureResource/tree/treeProvider'; +import { registerAzureResourceCommands } from '../azureResource/commands'; +import { AzureResourceAccountService } from '../azureResource/services/accountService'; +import { AzureResourceSubscriptionService } from '../azureResource/services/subscriptionService'; +import { AzureResourceSubscriptionFilterService } from '../azureResource/services/subscriptionFilterService'; +import { AzureResourceCacheService } from '../azureResource/services/cacheService'; +import { AzureResourceTenantService } from '../azureResource/services/tenantService'; + +import { registerAzureResourceDatabaseServerCommands } from '../azureResource/providers/databaseServer/commands'; +import { registerAzureResourceDatabaseCommands } from '../azureResource/providers/database/commands'; + +export default class AzureResourceController extends ControllerBase { + public activate(): Promise { + this.appContext.registerService(AzureResourceServiceNames.cacheService, new AzureResourceCacheService(this.extensionContext)); + this.appContext.registerService(AzureResourceServiceNames.accountService, new AzureResourceAccountService(this.apiWrapper)); + this.appContext.registerService(AzureResourceServiceNames.subscriptionService, new AzureResourceSubscriptionService()); + this.appContext.registerService(AzureResourceServiceNames.subscriptionFilterService, new AzureResourceSubscriptionFilterService(new AzureResourceCacheService(this.extensionContext))); + this.appContext.registerService(AzureResourceServiceNames.tenantService, new AzureResourceTenantService()); + + const azureResourceTree = new AzureResourceTreeProvider(this.appContext); + this.extensionContext.subscriptions.push(this.apiWrapper.registerTreeDataProvider('azureResourceExplorer', azureResourceTree)); + + this.appContext.getService(AzureResourceServiceNames.accountService).onDidChangeAccounts((e: DidChangeAccountsParams) => { azureResourceTree.notifyNodeChanged(undefined); }); + + registerAzureResourceCommands(this.appContext, azureResourceTree); + + registerAzureResourceDatabaseServerCommands(this.appContext); + + registerAzureResourceDatabaseCommands(this.appContext); + + return Promise.resolve(true); + } + + public deactivate(): void { + } +} diff --git a/extensions/azurecore/src/controllers/mainController.ts b/extensions/azurecore/src/controllers/mainController.ts deleted file mode 100644 index 7f38fd8667..0000000000 --- a/extensions/azurecore/src/controllers/mainController.ts +++ /dev/null @@ -1,54 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import ControllerBase from './controllerBase'; - -import { AzureResourceTreeProvider } from '../azureResource/tree/treeProvider'; -import { registerAzureResourceCommands } from '../azureResource/commands'; -import { AzureResourceServicePool } from '../azureResource/servicePool'; -import { AzureResourceCredentialService } from '../azureResource/services/credentialService'; -import { AzureResourceAccountService } from '../azureResource/services/accountService'; -import { AzureResourceSubscriptionService } from '../azureResource/services/subscriptionService'; -import { AzureResourceSubscriptionFilterService } from '../azureResource/services/subscriptionFilterService'; -import { AzureResourceDatabaseServerService } from '../azureResource/services/databaseServerService'; -import { AzureResourceDatabaseService } from '../azureResource/services/databaseService'; -import { AzureResourceCacheService } from '../azureResource/services/cacheService'; -import { AzureResourceContextService } from '../azureResource/services/contextService'; - -/** - * The main controller class that initializes the extension - */ -export default class MainController extends ControllerBase { - // PUBLIC METHODS ////////////////////////////////////////////////////// - /** - * Deactivates the extension - */ - public deactivate(): void { - } - - public activate(): Promise { - this.configureAzureResource(); - return Promise.resolve(true); - } - - private configureAzureResource(): void { - let servicePool = AzureResourceServicePool.getInstance(); - servicePool.cacheService = new AzureResourceCacheService(this.extensionContext); - servicePool.contextService = new AzureResourceContextService(this.extensionContext, this.apiWrapper); - servicePool.accountService = new AzureResourceAccountService(this.apiWrapper); - servicePool.credentialService = new AzureResourceCredentialService(this.apiWrapper); - servicePool.subscriptionService = new AzureResourceSubscriptionService(); - servicePool.subscriptionFilterService = new AzureResourceSubscriptionFilterService(new AzureResourceCacheService(this.extensionContext)); - servicePool.databaseService = new AzureResourceDatabaseService(); - servicePool.databaseServerService = new AzureResourceDatabaseServerService(); - - let azureResourceTree = new AzureResourceTreeProvider(); - this.extensionContext.subscriptions.push(this.apiWrapper.registerTreeDataProvider('azureResourceExplorer', azureResourceTree)); - - registerAzureResourceCommands(this.apiWrapper, azureResourceTree); - } -} diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts index a2e4463648..62e0c7c8e4 100644 --- a/extensions/azurecore/src/extension.ts +++ b/extensions/azurecore/src/extension.ts @@ -6,12 +6,17 @@ import * as path from 'path'; import * as os from 'os'; import * as constants from './constants'; -import MainController from './controllers/mainController'; +import AzureResourceController from './controllers/azureResourceController'; import { AppContext } from './appContext'; import ControllerBase from './controllers/controllerBase'; import { ApiWrapper } from './apiWrapper'; import { AzureAccountProviderService } from './account-provider/azureAccountProviderService'; +import { AzureResourceDatabaseServerProvider } from './azureResource/providers/databaseServer/databaseServerProvider'; +import { AzureResourceDatabaseServerService } from './azureResource/providers/databaseServer/databaseServerService'; +import { AzureResourceDatabaseProvider } from './azureResource/providers/database/databaseProvider'; +import { AzureResourceDatabaseService } from './azureResource/providers/database/databaseService'; + let controllers: ControllerBase[] = []; @@ -35,7 +40,8 @@ export function getDefaultLogLocation() { // this method is called when your extension is activated // your extension is activated the very first time the command is executed export function activate(extensionContext: vscode.ExtensionContext) { - let appContext = new AppContext(extensionContext, new ApiWrapper()); + const apiWrapper = new ApiWrapper(); + let appContext = new AppContext(extensionContext, apiWrapper); let activations: Thenable[] = []; // Create the folder for storing the token caches @@ -56,21 +62,19 @@ export function activate(extensionContext: vscode.ExtensionContext) { extensionContext.subscriptions.push(accountProviderService); accountProviderService.activate(); - // Start the main controller - let mainController = new MainController(appContext); - controllers.push(mainController); - extensionContext.subscriptions.push(mainController); - activations.push(mainController.activate()); + const azureResourceController = new AzureResourceController(appContext); + controllers.push(azureResourceController); + extensionContext.subscriptions.push(azureResourceController); + activations.push(azureResourceController.activate()); - return Promise.all(activations) - .then((results: boolean[]) => { - for (let result of results) { - if (!result) { - return false; - } - } - return true; - }); + return { + provideResources() { + return [ + new AzureResourceDatabaseServerProvider(new AzureResourceDatabaseServerService(), apiWrapper, extensionContext), + new AzureResourceDatabaseProvider(new AzureResourceDatabaseService(), apiWrapper, extensionContext) + ]; + } + }; } // this method is called when your extension is deactivated diff --git a/extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/messageTreeNode.test.ts similarity index 90% rename from extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts rename to extensions/azurecore/src/test/azureResource/messageTreeNode.test.ts index ddd867c60f..9f8c08e4c3 100644 --- a/extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts +++ b/extensions/azurecore/src/test/azureResource/messageTreeNode.test.ts @@ -9,8 +9,8 @@ import * as should from 'should'; import * as vscode from 'vscode'; import 'mocha'; -import { AzureResourceItemType } from '../../../azureResource/constants'; -import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode'; +import { AzureResourceItemType } from '../../azureResource/constants'; +import { AzureResourceMessageTreeNode } from '../../azureResource/messageTreeNode'; describe('AzureResourceMessageTreeNode.info', function(): void { it('Should be correct when created.', async function(): Promise { diff --git a/extensions/azurecore/src/test/azureResource/providers/database/databaseTreeDataProvider.test.ts b/extensions/azurecore/src/test/azureResource/providers/database/databaseTreeDataProvider.test.ts new file mode 100644 index 0000000000..463227da8c --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/providers/database/databaseTreeDataProvider.test.ts @@ -0,0 +1,150 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as should from 'should'; +import * as TypeMoq from 'typemoq'; +import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; +import 'mocha'; + +import { azureResource } from '../../../../azureResource/azure-resource'; +import { ApiWrapper } from '../../../../apiWrapper'; +import { IAzureResourceDatabaseService } from '../../../../azureResource/providers/database/interfaces'; +import { AzureResourceDatabaseTreeDataProvider } from '../../../../azureResource/providers/database/databaseTreeDataProvider'; +import { AzureResourceDatabase } from '../../../../azureResource/providers/database/models'; +import { AzureResourceItemType } from '../../../../azureResource/constants'; + +// Mock services +let mockDatabaseService: TypeMoq.IMock; +let mockApiWrapper: TypeMoq.IMock; +let mockExtensionContext: TypeMoq.IMock; + +// Mock test data +const mockAccount: sqlops.Account = { + key: { + accountId: 'mock_account', + providerId: 'mock_provider' + }, + displayInfo: { + displayName: 'mock_account@test.com', + accountType: 'Microsoft', + contextualDisplayName: 'test' + }, + properties: undefined, + isStale: false +}; + +const mockSubscription: azureResource.AzureResourceSubscription = { + id: 'mock_subscription', + name: 'mock subscription' +}; + +const mockTenantId: string = 'mock_tenant'; + +const mockResourceRootNode: azureResource.IAzureResourceNode = { + account: mockAccount, + subscription: mockSubscription, + tenantId: mockTenantId, + treeItem: { + id: 'mock_resource_root_node', + label: 'mock resource root node', + iconPath: undefined, + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextValue: 'mock_resource_root_node' + } +}; + +const mockTokens = {}; +mockTokens[mockTenantId] = { + token: 'mock_token', + tokenType: 'Bearer' +}; + +const mockDatabases: AzureResourceDatabase[] = [ + { + name: 'mock database 1', + serverName: 'mock database server 1', + serverFullName: 'mock database server full name 1', + loginName: 'mock login' + }, + { + name: 'mock database 2', + serverName: 'mock database server 2', + serverFullName: 'mock database server full name 2', + loginName: 'mock login' + } +]; + +describe('AzureResourceDatabaseTreeDataProvider.info', function(): void { + beforeEach(() => { + mockDatabaseService = TypeMoq.Mock.ofType(); + mockApiWrapper = TypeMoq.Mock.ofType(); + mockExtensionContext = TypeMoq.Mock.ofType(); + }); + + it('Should be correct when created.', async function(): Promise { + const treeDataProvider = new AzureResourceDatabaseTreeDataProvider(mockDatabaseService.object, mockApiWrapper.object, mockExtensionContext.object); + + const treeItem = await treeDataProvider.getTreeItem(mockResourceRootNode); + should(treeItem.id).equal(mockResourceRootNode.treeItem.id); + should(treeItem.label).equal(mockResourceRootNode.treeItem.label); + should(treeItem.collapsibleState).equal(mockResourceRootNode.treeItem.collapsibleState); + should(treeItem.contextValue).equal(mockResourceRootNode.treeItem.contextValue); + }); +}); + +describe('AzureResourceDatabaseTreeDataProvider.getChildren', function(): void { + beforeEach(() => { + mockDatabaseService = TypeMoq.Mock.ofType(); + mockApiWrapper = TypeMoq.Mock.ofType(); + mockExtensionContext = TypeMoq.Mock.ofType(); + + mockApiWrapper.setup((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockTokens)); + mockDatabaseService.setup((o) => o.getDatabases(mockSubscription, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockDatabases)); + mockExtensionContext.setup((o) => o.asAbsolutePath(TypeMoq.It.isAnyString())).returns(() => TypeMoq.It.isAnyString()); + }); + + it('Should return container node when element is undefined.', async function(): Promise { + const treeDataProvider = new AzureResourceDatabaseTreeDataProvider(mockDatabaseService.object, mockApiWrapper.object, mockExtensionContext.object); + + const children = await treeDataProvider.getChildren(); + + should(children).Array(); + should(children.length).equal(1); + + const child = children[0]; + should(child.account).undefined(); + should(child.subscription).undefined(); + should(child.tenantId).undefined(); + should(child.treeItem.id).equal('azure.resource.providers.database.treeDataProvider.databaseContainer'); + should(child.treeItem.label).equal('SQL Databases'); + should(child.treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); + should(child.treeItem.contextValue).equal('azure.resource.itemType.databaseContainer'); + }); + + it('Should return resource nodes when it is container node.', async function(): Promise { + const treeDataProvider = new AzureResourceDatabaseTreeDataProvider(mockDatabaseService.object, mockApiWrapper.object, mockExtensionContext.object); + + const children = await treeDataProvider.getChildren(mockResourceRootNode); + + should(children).Array(); + should(children.length).equal(mockDatabases.length); + + for (let ix = 0; ix < children.length; ix++) { + const child = children[ix]; + const database = mockDatabases[ix]; + + should(child.account).equal(mockAccount); + should(child.subscription).equal(mockSubscription); + should(child.tenantId).equal(mockTenantId); + should(child.treeItem.id).equal(`databaseServer_${database.serverFullName}.database_${database.name}`); + should(child.treeItem.label).equal(`${database.name} (${database.serverName})`); + should(child.treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None); + should(child.treeItem.contextValue).equal(AzureResourceItemType.database); + } + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/providers/databaseServer/databaseServerTreeDataProvider.test.ts b/extensions/azurecore/src/test/azureResource/providers/databaseServer/databaseServerTreeDataProvider.test.ts new file mode 100644 index 0000000000..4ded7d18f5 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/providers/databaseServer/databaseServerTreeDataProvider.test.ts @@ -0,0 +1,150 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as should from 'should'; +import * as TypeMoq from 'typemoq'; +import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; +import 'mocha'; + +import { azureResource } from '../../../../azureResource/azure-resource'; +import { ApiWrapper } from '../../../../apiWrapper'; +import { IAzureResourceDatabaseServerService } from '../../../../azureResource/providers/databaseServer/interfaces'; +import { AzureResourceDatabaseServerTreeDataProvider } from '../../../../azureResource/providers/databaseServer/databaseServerTreeDataProvider'; +import { AzureResourceDatabaseServer } from '../../../../azureResource/providers/databaseServer/models'; +import { AzureResourceItemType } from '../../../../azureResource/constants'; + +// Mock services +let mockDatabaseServerService: TypeMoq.IMock; +let mockApiWrapper: TypeMoq.IMock; +let mockExtensionContext: TypeMoq.IMock; + +// Mock test data +const mockAccount: sqlops.Account = { + key: { + accountId: 'mock_account', + providerId: 'mock_provider' + }, + displayInfo: { + displayName: 'mock_account@test.com', + accountType: 'Microsoft', + contextualDisplayName: 'test' + }, + properties: undefined, + isStale: false +}; + +const mockSubscription: azureResource.AzureResourceSubscription = { + id: 'mock_subscription', + name: 'mock subscription' +}; + +const mockTenantId: string = 'mock_tenant'; + +const mockResourceRootNode: azureResource.IAzureResourceNode = { + account: mockAccount, + subscription: mockSubscription, + tenantId: mockTenantId, + treeItem: { + id: 'mock_resource_root_node', + label: 'mock resource root node', + iconPath: undefined, + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextValue: 'mock_resource_root_node' + } +}; + +const mockTokens = {}; +mockTokens[mockTenantId] = { + token: 'mock_token', + tokenType: 'Bearer' +}; + +const mockDatabaseServers: AzureResourceDatabaseServer[] = [ + { + name: 'mock database server 1', + fullName: 'mock database server full name 1', + loginName: 'mock login', + defaultDatabaseName: 'master' + }, + { + name: 'mock database server 2', + fullName: 'mock database server full name 2', + loginName: 'mock login', + defaultDatabaseName: 'master' + } +]; + +describe('AzureResourceDatabaseServerTreeDataProvider.info', function(): void { + beforeEach(() => { + mockDatabaseServerService = TypeMoq.Mock.ofType(); + mockApiWrapper = TypeMoq.Mock.ofType(); + mockExtensionContext = TypeMoq.Mock.ofType(); + }); + + it('Should be correct when created.', async function(): Promise { + const treeDataProvider = new AzureResourceDatabaseServerTreeDataProvider(mockDatabaseServerService.object, mockApiWrapper.object, mockExtensionContext.object); + + const treeItem = await treeDataProvider.getTreeItem(mockResourceRootNode); + should(treeItem.id).equal(mockResourceRootNode.treeItem.id); + should(treeItem.label).equal(mockResourceRootNode.treeItem.label); + should(treeItem.collapsibleState).equal(mockResourceRootNode.treeItem.collapsibleState); + should(treeItem.contextValue).equal(mockResourceRootNode.treeItem.contextValue); + }); +}); + +describe('AzureResourceDatabaseServerTreeDataProvider.getChildren', function(): void { + beforeEach(() => { + mockDatabaseServerService = TypeMoq.Mock.ofType(); + mockApiWrapper = TypeMoq.Mock.ofType(); + mockExtensionContext = TypeMoq.Mock.ofType(); + + mockApiWrapper.setup((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockTokens)); + mockDatabaseServerService.setup((o) => o.getDatabaseServers(mockSubscription, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockDatabaseServers)); + mockExtensionContext.setup((o) => o.asAbsolutePath(TypeMoq.It.isAnyString())).returns(() => TypeMoq.It.isAnyString()); + }); + + it('Should return container node when element is undefined.', async function(): Promise { + const treeDataProvider = new AzureResourceDatabaseServerTreeDataProvider(mockDatabaseServerService.object, mockApiWrapper.object, mockExtensionContext.object); + + const children = await treeDataProvider.getChildren(); + + should(children).Array(); + should(children.length).equal(1); + + const child = children[0]; + should(child.account).undefined(); + should(child.subscription).undefined(); + should(child.tenantId).undefined(); + should(child.treeItem.id).equal('azure.resource.providers.databaseServer.treeDataProvider.databaseServerContainer'); + should(child.treeItem.label).equal('SQL Servers'); + should(child.treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); + should(child.treeItem.contextValue).equal('azure.resource.itemType.databaseServerContainer'); + }); + + it('Should return resource nodes when it is container node.', async function(): Promise { + const treeDataProvider = new AzureResourceDatabaseServerTreeDataProvider(mockDatabaseServerService.object, mockApiWrapper.object, mockExtensionContext.object); + + const children = await treeDataProvider.getChildren(mockResourceRootNode); + + should(children).Array(); + should(children.length).equal(mockDatabaseServers.length); + + for (let ix = 0; ix < children.length; ix++) { + const child = children[ix]; + const databaseServer = mockDatabaseServers[ix]; + + should(child.account).equal(mockAccount); + should(child.subscription).equal(mockSubscription); + should(child.tenantId).equal(mockTenantId); + should(child.treeItem.id).equal(`databaseServer_${databaseServer.name}`); + should(child.treeItem.label).equal(databaseServer.name); + should(child.treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None); + should(child.treeItem.contextValue).equal(AzureResourceItemType.databaseServer); + } + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/resourceService.test.ts b/extensions/azurecore/src/test/azureResource/resourceService.test.ts new file mode 100644 index 0000000000..f6607383cc --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/resourceService.test.ts @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as should from 'should'; +import * as TypeMoq from 'typemoq'; +import * as sqlops from 'sqlops'; +import 'mocha'; +import { fail } from 'assert'; + +import { azureResource } from '../../azureResource/azure-resource'; +import { AzureResourceService } from '../../azureResource/resourceService'; + +// Mock test data +const mockAccount: sqlops.Account = { + key: { + accountId: 'mock_account', + providerId: 'mock_provider' + }, + displayInfo: { + displayName: 'mock_account@test.com', + accountType: 'Microsoft', + contextualDisplayName: 'test' + }, + properties: undefined, + isStale: false +}; + +const mockSubscription: azureResource.AzureResourceSubscription = { + id: 'mock_subscription', + name: 'mock subscription' +}; + +const mockTenantId: string = 'mock_tenant'; + +let mockResourceTreeDataProvider1: TypeMoq.IMock; +let mockResourceProvider1: TypeMoq.IMock; + +let mockResourceTreeDataProvider2: TypeMoq.IMock; +let mockResourceProvider2: TypeMoq.IMock; + +const resourceService: AzureResourceService = AzureResourceService.getInstance(); + +describe('AzureResourceService.listResourceProviderIds', function(): void { + beforeEach(() => { + mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType(); + mockResourceTreeDataProvider1.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType().object])); + mockResourceTreeDataProvider1.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny())); + mockResourceProvider1 = TypeMoq.Mock.ofType(); + mockResourceProvider1.setup((o) => o.providerId).returns(() => 'mockResourceProvider1'); + mockResourceProvider1.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider1.object); + + mockResourceTreeDataProvider2 = TypeMoq.Mock.ofType(); + mockResourceTreeDataProvider2.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType().object])); + mockResourceTreeDataProvider2.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny())); + mockResourceProvider2 = TypeMoq.Mock.ofType(); + mockResourceProvider2.setup((o) => o.providerId).returns(() => 'mockResourceProvider2'); + mockResourceProvider2.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider2.object); + + resourceService.clearResourceProviders(); + resourceService.areResourceProvidersLoaded = true; + }); + + it('Should be correct when registering providers.', async function(): Promise { + resourceService.registerResourceProvider(mockResourceProvider1.object); + let providerIds = await resourceService.listResourceProviderIds(); + should(providerIds).Array(); + should(providerIds.length).equal(1); + should(providerIds[0]).equal(mockResourceProvider1.object.providerId); + + resourceService.registerResourceProvider(mockResourceProvider2.object); + providerIds = await resourceService.listResourceProviderIds(); + should(providerIds).Array(); + should(providerIds.length).equal(2); + should(providerIds[0]).equal(mockResourceProvider1.object.providerId); + should(providerIds[1]).equal(mockResourceProvider2.object.providerId); + }); +}); + +describe('AzureResourceService.getRootChildren', function(): void { + beforeEach(() => { + mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType(); + mockResourceTreeDataProvider1.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType().object])); + mockResourceTreeDataProvider1.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny())); + mockResourceProvider1 = TypeMoq.Mock.ofType(); + mockResourceProvider1.setup((o) => o.providerId).returns(() => 'mockResourceProvider1'); + mockResourceProvider1.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider1.object); + + resourceService.clearResourceProviders(); + resourceService.registerResourceProvider(mockResourceProvider1.object); + resourceService.areResourceProvidersLoaded = true; + }); + + it('Should be correct when provider id is correct.', async function(): Promise { + const children = await resourceService.getRootChildren(mockResourceProvider1.object.providerId, mockAccount, mockSubscription, mockTenantId); + + should(children).Array(); + }); + + it('Should throw exceptions when provider id is incorrect.', async function(): Promise { + const providerId = 'non_existent_provider_id'; + try { + await resourceService.getRootChildren(providerId, mockAccount, mockSubscription, mockTenantId); + } catch (error) { + should(error.message).equal(`Azure resource provider doesn't exist. Id: ${providerId}`); + return; + } + + fail(); + }); +}); + +describe('AzureResourceService.getChildren', function(): void { + beforeEach(() => { + mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType(); + mockResourceTreeDataProvider1.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType().object])); + mockResourceTreeDataProvider1.setup((o) => o.getChildren(TypeMoq.It.isAny())).returns(() => Promise.resolve([TypeMoq.Mock.ofType().object])); + mockResourceTreeDataProvider1.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny())); + mockResourceProvider1 = TypeMoq.Mock.ofType(); + mockResourceProvider1.setup((o) => o.providerId).returns(() => 'mockResourceProvider1'); + mockResourceProvider1.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider1.object); + + resourceService.clearResourceProviders(); + resourceService.registerResourceProvider(mockResourceProvider1.object); + resourceService.areResourceProvidersLoaded = true; + }); + + it('Should be correct when provider id is correct.', async function(): Promise { + const children = await resourceService.getChildren(mockResourceProvider1.object.providerId, TypeMoq.It.isAny()); + should(children).Array(); + }); + + it('Should throw exceptions when provider id is incorrect.', async function(): Promise { + const providerId = 'non_existent_provider_id'; + try { + await resourceService.getRootChildren(providerId, mockAccount, mockSubscription, mockTenantId); + } catch (error) { + should(error.message).equal(`Azure resource provider doesn't exist. Id: ${providerId}`); + return; + } + + fail(); + }); +}); + +describe('AzureResourceService.getTreeItem', function(): void { + beforeEach(() => { + mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType(); + mockResourceTreeDataProvider1.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType().object])); + mockResourceTreeDataProvider1.setup((o) => o.getChildren(TypeMoq.It.isAny())).returns(() => Promise.resolve([TypeMoq.Mock.ofType().object])); + mockResourceTreeDataProvider1.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny())); + mockResourceProvider1 = TypeMoq.Mock.ofType(); + mockResourceProvider1.setup((o) => o.providerId).returns(() => 'mockResourceProvider1'); + mockResourceProvider1.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider1.object); + + resourceService.clearResourceProviders(); + resourceService.registerResourceProvider(mockResourceProvider1.object); + resourceService.areResourceProvidersLoaded = true; + }); + + it('Should be correct when provider id is correct.', async function(): Promise { + const treeItem = await resourceService.getTreeItem(mockResourceProvider1.object.providerId, TypeMoq.It.isAny()); + should(treeItem).Object(); + }); + + it('Should throw exceptions when provider id is incorrect.', async function(): Promise { + const providerId = 'non_existent_provider_id'; + try { + await resourceService.getRootChildren(providerId, mockAccount, mockSubscription, mockTenantId); + } catch (error) { + should(error.message).equal(`Azure resource provider doesn't exist. Id: ${providerId}`); + return; + } + + fail(); + }); +}); \ No newline at end of file diff --git a/extensions/azurecore/src/test/azureResource/resourceTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/resourceTreeNode.test.ts new file mode 100644 index 0000000000..511cd8284b --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/resourceTreeNode.test.ts @@ -0,0 +1,184 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as should from 'should'; +import * as TypeMoq from 'typemoq'; +import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; +import 'mocha'; + +import { azureResource } from '../../azureResource/azure-resource'; +import { AzureResourceService } from '../../azureResource/resourceService'; +import { AzureResourceResourceTreeNode } from '../../azureResource/resourceTreeNode'; + +const resourceService = AzureResourceService.getInstance(); + +// Mock test data +const mockAccount: sqlops.Account = { + key: { + accountId: 'mock_account', + providerId: 'mock_provider' + }, + displayInfo: { + displayName: 'mock_account@test.com', + accountType: 'Microsoft', + contextualDisplayName: 'test' + }, + properties: undefined, + isStale: false +}; + +const mockSubscription: azureResource.AzureResourceSubscription = { + id: 'mock_subscription', + name: 'mock subscription' +}; + +const mockTenantId: string = 'mock_tenant'; + +const mockResourceProviderId: string = 'mock_resource_provider'; + +const mockResourceRootNode: azureResource.IAzureResourceNode = { + account: mockAccount, + subscription: mockSubscription, + tenantId: mockTenantId, + treeItem: { + id: 'mock_resource_root_node', + label: 'mock resource root node', + iconPath: undefined, + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextValue: 'mock_resource_root_node' + } +}; + +const mockResourceNode1: azureResource.IAzureResourceNode = { + account: mockAccount, + subscription: mockSubscription, + tenantId: mockTenantId, + treeItem: { + id: 'mock_resource_node_1', + label: 'mock resource node 1', + iconPath: undefined, + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextValue: 'mock_resource_node' + } +}; + +const mockResourceNode2: azureResource.IAzureResourceNode = { + account: mockAccount, + subscription: mockSubscription, + tenantId: mockTenantId, + treeItem: { + id: 'mock_resource_node_2', + label: 'mock resource node 2', + iconPath: undefined, + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextValue: 'mock_resource_node' + } +}; + +const mockResourceNodes: azureResource.IAzureResourceNode[] = [mockResourceNode1, mockResourceNode2]; + +let mockResourceTreeDataProvider: TypeMoq.IMock; +let mockResourceProvider: TypeMoq.IMock; + +describe('AzureResourceResourceTreeNode.info', function(): void { + beforeEach(() => { + mockResourceTreeDataProvider = TypeMoq.Mock.ofType(); + mockResourceTreeDataProvider.setup((o) => o.getTreeItem(mockResourceRootNode)).returns(() => mockResourceRootNode.treeItem); + mockResourceTreeDataProvider.setup((o) => o.getChildren(mockResourceRootNode)).returns(() => Promise.resolve(mockResourceNodes)); + + mockResourceProvider = TypeMoq.Mock.ofType(); + mockResourceProvider.setup((o) => o.providerId).returns(() => mockResourceProviderId); + mockResourceProvider.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider.object); + + resourceService.clearResourceProviders(); + resourceService.registerResourceProvider(mockResourceProvider.object); + + resourceService.areResourceProvidersLoaded = true; + }); + + it('Should be correct when created.', async function(): Promise { + const resourceTreeNode = new AzureResourceResourceTreeNode({ + resourceProviderId: mockResourceProviderId, + resourceNode: mockResourceRootNode + }, undefined); + + should(resourceTreeNode.nodePathValue).equal(mockResourceRootNode.treeItem.id); + + const treeItem = await resourceTreeNode.getTreeItem(); + should(treeItem.id).equal(mockResourceRootNode.treeItem.id); + should(treeItem.label).equal(mockResourceRootNode.treeItem.label); + should(treeItem.collapsibleState).equal(mockResourceRootNode.treeItem.collapsibleState); + should(treeItem.contextValue).equal(mockResourceRootNode.treeItem.contextValue); + + const nodeInfo = resourceTreeNode.getNodeInfo(); + should(nodeInfo.label).equal(mockResourceRootNode.treeItem.label); + should(nodeInfo.isLeaf).equal(mockResourceRootNode.treeItem.collapsibleState === vscode.TreeItemCollapsibleState.None); + should(nodeInfo.nodeType).equal(mockResourceRootNode.treeItem.contextValue); + should(nodeInfo.iconType).equal(mockResourceRootNode.treeItem.contextValue); + }); +}); + +describe('AzureResourceResourceTreeNode.getChildren', function(): void { + beforeEach(() => { + mockResourceTreeDataProvider = TypeMoq.Mock.ofType(); + mockResourceTreeDataProvider.setup((o) => o.getChildren(mockResourceRootNode)).returns(() => Promise.resolve(mockResourceNodes)); + + mockResourceProvider = TypeMoq.Mock.ofType(); + mockResourceProvider.setup((o) => o.providerId).returns(() => mockResourceProviderId); + mockResourceProvider.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider.object); + + resourceService.clearResourceProviders(); + resourceService.registerResourceProvider(mockResourceProvider.object); + + resourceService.areResourceProvidersLoaded = true; + }); + + it('Should return resource nodes when it is container node.', async function(): Promise { + const resourceTreeNode = new AzureResourceResourceTreeNode({ + resourceProviderId: mockResourceProviderId, + resourceNode: mockResourceRootNode + }, undefined); + + const children = await resourceTreeNode.getChildren(); + + mockResourceTreeDataProvider.verify((o) => o.getChildren(mockResourceRootNode), TypeMoq.Times.once()); + + should(children).Array(); + should(children.length).equal(mockResourceNodes.length); + + for (let ix = 0; ix < children.length; ix++) { + const child = children[ix]; + + should(child).instanceOf(AzureResourceResourceTreeNode); + + const childNode = (child as AzureResourceResourceTreeNode).resourceNodeWithProviderId; + should(childNode.resourceProviderId).equal(mockResourceProviderId); + should(childNode.resourceNode.account).equal(mockAccount); + should(childNode.resourceNode.subscription).equal(mockSubscription); + should(childNode.resourceNode.tenantId).equal(mockTenantId); + should(childNode.resourceNode.treeItem.id).equal(mockResourceNodes[ix].treeItem.id); + should(childNode.resourceNode.treeItem.label).equal(mockResourceNodes[ix].treeItem.label); + should(childNode.resourceNode.treeItem.collapsibleState).equal(mockResourceNodes[ix].treeItem.collapsibleState); + should(childNode.resourceNode.treeItem.contextValue).equal(mockResourceNodes[ix].treeItem.contextValue); + } + }); + + it('Should return empty when it is leaf node.', async function(): Promise { + const resourceTreeNode = new AzureResourceResourceTreeNode({ + resourceProviderId: mockResourceProviderId, + resourceNode: mockResourceNode1 + }, undefined); + + const children = await resourceTreeNode.getChildren(); + + mockResourceTreeDataProvider.verify((o) => o.getChildren(), TypeMoq.Times.exactly(0)); + + should(children).Array(); + should(children.length).equal(0); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts index 9a3cb3f866..5cb0d1cd15 100644 --- a/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts +++ b/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts @@ -26,7 +26,7 @@ describe('AzureResourceAccountNotSignedInTreeNode.info', function(): void { should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None); should(treeItem.command).not.undefined(); should(treeItem.command.title).equal(label); - should(treeItem.command.command).equal('azureresource.signin'); + should(treeItem.command.command).equal('azure.resource.signin'); const nodeInfo = treeNode.getNodeInfo(); should(nodeInfo.isLeaf).true(); diff --git a/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts index a140b98171..28665dc5b5 100644 --- a/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts +++ b/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts @@ -10,35 +10,38 @@ import * as TypeMoq from 'typemoq'; import * as sqlops from 'sqlops'; import * as vscode from 'vscode'; import 'mocha'; -import { ServiceClientCredentials } from 'ms-rest'; +import { TokenCredentials } from 'ms-rest'; +import { AppContext } from '../../../appContext'; -import { AzureResourceServicePool } from '../../../azureResource/servicePool'; +import { azureResource } from '../../../azureResource/azure-resource'; import { IAzureResourceCacheService, - IAzureResourceContextService, - IAzureResourceCredentialService, IAzureResourceSubscriptionService, - IAzureResourceSubscriptionFilterService + IAzureResourceSubscriptionFilterService, + IAzureResourceTenantService } from '../../../azureResource/interfaces'; import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler'; import { AzureResourceAccountTreeNode } from '../../../azureResource/tree/accountTreeNode'; -import { AzureResourceSubscription } from '../../../azureResource/models'; import { AzureResourceSubscriptionTreeNode } from '../../../azureResource/tree/subscriptionTreeNode'; -import { AzureResourceItemType } from '../../../azureResource/constants'; -import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode'; +import { AzureResourceItemType, AzureResourceServiceNames } from '../../../azureResource/constants'; +import { AzureResourceMessageTreeNode } from '../../../azureResource/messageTreeNode'; +import { ApiWrapper } from '../../../apiWrapper'; +import { generateGuid } from '../../../azureResource/utils'; // Mock services -const mockServicePool = AzureResourceServicePool.getInstance(); - +let mockExtensionContext: TypeMoq.IMock; +let mockApiWrapper: TypeMoq.IMock; let mockCacheService: TypeMoq.IMock; -let mockContextService: TypeMoq.IMock; -let mockCredentialService: TypeMoq.IMock; let mockSubscriptionService: TypeMoq.IMock; let mockSubscriptionFilterService: TypeMoq.IMock; +let mockTenantService: TypeMoq.IMock; +let mockAppContext: AppContext; let mockTreeChangeHandler: TypeMoq.IMock; // Mock test data +const mockTenantId = 'mock_tenant_id'; + const mockAccount: sqlops.Account = { key: { accountId: 'mock_account', @@ -49,51 +52,68 @@ const mockAccount: sqlops.Account = { accountType: 'Microsoft', contextualDisplayName: 'test' }, - properties: undefined, + properties: { + tenants: [ + { + id: mockTenantId + } + ] + }, isStale: false }; -const mockCredential = TypeMoq.Mock.ofType().object; -const mockCredentials = [mockCredential]; - -const mockSubscription1: AzureResourceSubscription = { +const mockSubscription1: azureResource.AzureResourceSubscription = { id: 'mock_subscription_1', name: 'mock subscription 1' }; -const mockSubscription2: AzureResourceSubscription = { + +const mockSubscription2: azureResource.AzureResourceSubscription = { id: 'mock_subscription_2', name: 'mock subscription 2' }; + const mockSubscriptions = [mockSubscription1, mockSubscription2]; + const mockFilteredSubscriptions = [mockSubscription1]; -let mockSubscriptionCache: { subscriptions: { [accountId: string]: AzureResourceSubscription[]} }; +const mockTokens = {}; +mockTokens[mockTenantId] = { + token: 'mock_token', + tokenType: 'Bearer' +}; + +const mockCredential = new TokenCredentials(mockTokens[mockTenantId].token, mockTokens[mockTenantId].tokenType); + +let mockSubscriptionCache: azureResource.AzureResourceSubscription[] = []; describe('AzureResourceAccountTreeNode.info', function(): void { beforeEach(() => { - mockContextService = TypeMoq.Mock.ofType(); + mockExtensionContext = TypeMoq.Mock.ofType(); + mockApiWrapper = TypeMoq.Mock.ofType(); mockCacheService = TypeMoq.Mock.ofType(); - mockCredentialService = TypeMoq.Mock.ofType(); mockSubscriptionService = TypeMoq.Mock.ofType(); mockSubscriptionFilterService = TypeMoq.Mock.ofType(); + mockTenantService = TypeMoq.Mock.ofType(); mockTreeChangeHandler = TypeMoq.Mock.ofType(); - mockSubscriptionCache = { subscriptions: {} }; + mockSubscriptionCache = []; - mockServicePool.contextService = mockContextService.object; - mockServicePool.cacheService = mockCacheService.object; - mockServicePool.credentialService = mockCredentialService.object; - mockServicePool.subscriptionService = mockSubscriptionService.object; - mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object; + mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object); + mockAppContext.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object); + mockAppContext.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object); + mockAppContext.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object); + mockAppContext.registerService(AzureResourceServiceNames.tenantService, mockTenantService.object); - mockCredentialService.setup((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockCredentials)); + mockApiWrapper.setup((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockTokens)); + mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid()); mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache); - mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions); + mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache = mockSubscriptions); + mockTenantService.setup((o) => o.getTenantId(TypeMoq.It.isAny())).returns(() => Promise.resolve(mockTenantId)); }); it('Should be correct when created.', async function(): Promise { - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const accountTreeNodeId = `account_${mockAccount.key.accountId}`; const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockAccount.key.accountId})`; @@ -114,14 +134,17 @@ describe('AzureResourceAccountTreeNode.info', function(): void { }); it('Should be correct when there are subscriptions listed.', async function(): Promise { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions)); mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined)); const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockAccount.key.accountId}) (${mockSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`; - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); - await accountTreeNode.getChildren(); + const subscriptionNodes = await accountTreeNode.getChildren(); + + should(subscriptionNodes).Array(); + should(subscriptionNodes.length).equal(mockSubscriptions.length); const treeItem = await accountTreeNode.getTreeItem(); should(treeItem.label).equal(accountTreeNodeLabel); @@ -131,14 +154,17 @@ describe('AzureResourceAccountTreeNode.info', function(): void { }); it('Should be correct when there are subscriptions filtered.', async function(): Promise { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions)); mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions)); const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockAccount.key.accountId}) (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`; - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); - await accountTreeNode.getChildren(); + const subscriptionNodes = await accountTreeNode.getChildren(); + + should(subscriptionNodes).Array(); + should(subscriptionNodes.length).equal(mockFilteredSubscriptions.length); const treeItem = await accountTreeNode.getTreeItem(); should(treeItem.label).equal(accountTreeNodeLabel); @@ -150,36 +176,41 @@ describe('AzureResourceAccountTreeNode.info', function(): void { describe('AzureResourceAccountTreeNode.getChildren', function(): void { beforeEach(() => { + mockExtensionContext = TypeMoq.Mock.ofType(); + mockApiWrapper = TypeMoq.Mock.ofType(); mockCacheService = TypeMoq.Mock.ofType(); - mockCredentialService = TypeMoq.Mock.ofType(); mockSubscriptionService = TypeMoq.Mock.ofType(); mockSubscriptionFilterService = TypeMoq.Mock.ofType(); + mockTenantService = TypeMoq.Mock.ofType(); mockTreeChangeHandler = TypeMoq.Mock.ofType(); - mockSubscriptionCache = { subscriptions: {} }; + mockSubscriptionCache = []; - mockServicePool.cacheService = mockCacheService.object; - mockServicePool.credentialService = mockCredentialService.object; - mockServicePool.subscriptionService = mockSubscriptionService.object; - mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object; + mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object); + mockAppContext.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object); + mockAppContext.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object); + mockAppContext.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object); + mockAppContext.registerService(AzureResourceServiceNames.tenantService, mockTenantService.object); - mockCredentialService.setup((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockCredentials)); + mockApiWrapper.setup((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockTokens)); + mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid()); mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache); - mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions); + mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache = mockSubscriptions); + mockTenantService.setup((o) => o.getTenantId(TypeMoq.It.isAny())).returns(() => Promise.resolve(mockTenantId)); }); it('Should load subscriptions from scratch and update cache when it is clearing cache.', async function(): Promise { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); - mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined)); + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([])); - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const children = await accountTreeNode.getChildren(); - mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once()); - mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once()); - mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + mockApiWrapper.verify((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once()); + mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredential), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(0)); mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once()); @@ -192,43 +223,42 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void { should(children).Array(); should(children.length).equal(mockSubscriptions.length); - should(Object.keys(mockSubscriptionCache.subscriptions)).deepEqual([mockAccount.key.accountId]); - should(mockSubscriptionCache.subscriptions[mockAccount.key.accountId]).deepEqual(mockSubscriptions); + should(mockSubscriptionCache).deepEqual(mockSubscriptions); for (let ix = 0; ix < mockSubscriptions.length; ix++) { const child = children[ix]; const subscription = mockSubscriptions[ix]; should(child).instanceof(AzureResourceSubscriptionTreeNode); - should(child.nodePathValue).equal(`subscription_${subscription.id}`); + should(child.nodePathValue).equal(`account_${mockAccount.key.accountId}.subscription_${subscription.id}.tenant_${mockTenantId}`); } }); it('Should load subscriptions from cache when it is not clearing cache.', async function(): Promise { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions)); mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined)); - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); await accountTreeNode.getChildren(); const children = await accountTreeNode.getChildren(); - mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.exactly(1)); - mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.exactly(1)); - mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2)); - mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1)); + mockApiWrapper.verify((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once()); + mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredential), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); - should(children.length).equal(mockSubscriptionCache.subscriptions[mockAccount.key.accountId].length); + should(children.length).equal(mockSubscriptionCache.length); - for (let ix = 0; ix < mockSubscriptionCache.subscriptions[mockAccount.key.accountId].length; ix++) { - should(children[ix].nodePathValue).equal(`subscription_${mockSubscriptionCache.subscriptions[mockAccount.key.accountId][ix].id}`); + for (let ix = 0; ix < mockSubscriptionCache.length; ix++) { + should(children[ix].nodePathValue).equal(`account_${mockAccount.key.accountId}.subscription_${mockSubscriptionCache[ix].id}.tenant_${mockTenantId}`); } }); it('Should handle when there is no subscriptions.', async function(): Promise { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(undefined)); + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(undefined)); - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const children = await accountTreeNode.getChildren(); @@ -242,10 +272,10 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void { }); it('Should honor subscription filtering.', async function(): Promise { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions)); mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions)); - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const children = await accountTreeNode.getChildren(); @@ -255,23 +285,25 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void { should(children.length).equal(mockFilteredSubscriptions.length); for (let ix = 0; ix < mockFilteredSubscriptions.length; ix++) { - should(children[ix].nodePathValue).equal(`subscription_${mockFilteredSubscriptions[ix].id}`); + should(children[ix].nodePathValue).equal(`account_${mockAccount.key.accountId}.subscription_${mockFilteredSubscriptions[ix].id}.tenant_${mockTenantId}`); } }); it('Should handle errors.', async function(): Promise { - const mockError = 'Test error'; - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => { throw new Error(mockError); }); + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions)); - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + const mockError = 'Test error'; + mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => { throw new Error(mockError); }); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const children = await accountTreeNode.getChildren(); - mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once()); - mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once()); + mockApiWrapper.verify((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once()); + mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredential), TypeMoq.Times.once()); + mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once()); mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never()); - mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.never()); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); should(children).Array(); should(children.length).equal(1); @@ -283,12 +315,33 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void { describe('AzureResourceAccountTreeNode.clearCache', function() : void { beforeEach(() => { + mockExtensionContext = TypeMoq.Mock.ofType(); + mockApiWrapper = TypeMoq.Mock.ofType(); + mockCacheService = TypeMoq.Mock.ofType(); + mockSubscriptionService = TypeMoq.Mock.ofType(); + mockSubscriptionFilterService = TypeMoq.Mock.ofType(); + mockTenantService = TypeMoq.Mock.ofType(); + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockSubscriptionCache = []; + + mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object); + mockAppContext.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object); + mockAppContext.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object); + mockAppContext.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object); + mockAppContext.registerService(AzureResourceServiceNames.tenantService, mockTenantService.object); + + mockApiWrapper.setup((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockTokens)); + mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid()); + mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache); + mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache = mockSubscriptions); + mockTenantService.setup((o) => o.getTenantId(TypeMoq.It.isAny())).returns(() => Promise.resolve(mockTenantId)); }); it('Should clear cache.', async function(): Promise { - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); accountTreeNode.clearCache(); should(accountTreeNode.isClearingCache).true(); }); -}); +}); \ No newline at end of file diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts deleted file mode 100644 index c41eaac2bb..0000000000 --- a/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts +++ /dev/null @@ -1,219 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as should from 'should'; -import * as TypeMoq from 'typemoq'; -import * as sqlops from 'sqlops'; -import * as vscode from 'vscode'; -import 'mocha'; -import { ServiceClientCredentials } from 'ms-rest'; - -import { AzureResourceServicePool } from '../../../azureResource/servicePool'; -import { - IAzureResourceCacheService, - IAzureResourceContextService, - IAzureResourceCredentialService, - IAzureResourceDatabaseService -} from '../../../azureResource/interfaces'; -import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler'; -import { AzureResourceSubscription, AzureResourceDatabase } from '../../../azureResource/models'; -import { AzureResourceItemType } from '../../../azureResource/constants'; -import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode'; -import { AzureResourceDatabaseContainerTreeNode } from '../../../azureResource/tree/databaseContainerTreeNode'; -import { AzureResourceDatabaseTreeNode } from '../../../azureResource/tree/databaseTreeNode'; - -// Mock services -const mockServicePool = AzureResourceServicePool.getInstance(); - -let mockCacheService: TypeMoq.IMock; -let mockContextService: TypeMoq.IMock; -let mockCredentialService: TypeMoq.IMock; -let mockDatabaseService: TypeMoq.IMock; - -let mockTreeChangeHandler: TypeMoq.IMock; - -// Mock test data -const mockAccount: sqlops.Account = { - key: { - accountId: 'mock_account', - providerId: 'mock_provider' - }, - displayInfo: { - displayName: 'mock_account@test.com', - accountType: 'Microsoft', - contextualDisplayName: 'test' - }, - properties: undefined, - isStale: false -}; - -const mockCredential = TypeMoq.Mock.ofType().object; -const mockCredentials = [mockCredential]; - -const mockSubscription: AzureResourceSubscription = { - id: 'mock_subscription', - name: 'mock subscription' -}; - -const mockDatabase1: AzureResourceDatabase = { - name: 'mock database 1', - serverName: 'mock server 1', - serverFullName: 'mock server 1', - loginName: 'mock user 1' -}; -const mockDatabase2: AzureResourceDatabase = { - name: 'mock database 2', - serverName: 'mock server 2', - serverFullName: 'mock server 2', - loginName: 'mock user 2' -}; -const mockDatabases = [mockDatabase1, mockDatabase2]; - -let mockDatabaseContainerCache: { databases: { [subscriptionId: string]: AzureResourceDatabase[] } }; - -describe('AzureResourceDatabaseContainerTreeNode.info', function(): void { - beforeEach(() => { - mockContextService = TypeMoq.Mock.ofType(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockServicePool.contextService = mockContextService.object; - }); - - it('Should be correct when created.', async function(): Promise { - const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - - const databaseContainerTreeNodeLabel = 'SQL Databases'; - - should(databaseContainerTreeNode.nodePathValue).equal('databaseContainer'); - - const treeItem = await databaseContainerTreeNode.getTreeItem(); - should(treeItem.label).equal(databaseContainerTreeNodeLabel); - should(treeItem.contextValue).equal(AzureResourceItemType.databaseContainer); - should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); - - const nodeInfo = databaseContainerTreeNode.getNodeInfo(); - should(nodeInfo.isLeaf).false(); - should(nodeInfo.label).equal(databaseContainerTreeNodeLabel); - should(nodeInfo.nodeType).equal(AzureResourceItemType.databaseContainer); - should(nodeInfo.iconType).equal(AzureResourceItemType.databaseContainer); - }); -}); - -describe('AzureResourceDatabaseContainerTreeNode.getChildren', function(): void { - beforeEach(() => { - mockCacheService = TypeMoq.Mock.ofType(); - mockCredentialService = TypeMoq.Mock.ofType(); - mockDatabaseService = TypeMoq.Mock.ofType(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockDatabaseContainerCache = { databases: {} }; - - mockServicePool.cacheService = mockCacheService.object; - mockServicePool.credentialService = mockCredentialService.object; - mockServicePool.databaseService = mockDatabaseService.object; - - mockCredentialService.setup((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockCredentials)); - mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockDatabaseContainerCache); - mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockDatabaseContainerCache.databases[mockSubscription.id] = mockDatabases); - }); - - it('Should load databases from scratch and update cache when it is clearing cache.', async function(): Promise { - mockDatabaseService.setup((o) => o.getDatabases(mockSubscription, mockCredentials)).returns(() => Promise.resolve(mockDatabases)); - - const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - - const children = await databaseContainerTreeNode.getChildren(); - - mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once()); - mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.once()); - mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); - mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); - - should(databaseContainerTreeNode.isClearingCache).false(); - - should(children).Array(); - should(children.length).equal(mockDatabases.length); - - should(Object.keys(mockDatabaseContainerCache.databases)).deepEqual([mockSubscription.id]); - should(mockDatabaseContainerCache.databases[mockSubscription.id]).deepEqual(mockDatabases); - - for (let ix = 0; ix < mockDatabases.length; ix++) { - const child = children[ix]; - const database = mockDatabases[ix]; - - should(child).instanceof(AzureResourceDatabaseTreeNode); - should(child.nodePathValue).equal(`database_${database.name}`); - } - }); - - it('Should load databases from cache when it is not clearing cache.', async function(): Promise { - mockDatabaseService.setup((o) => o.getDatabases(mockSubscription, mockCredentials)).returns(() => Promise.resolve(mockDatabases)); - - const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - - await databaseContainerTreeNode.getChildren(); - const children = await databaseContainerTreeNode.getChildren(); - - mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.exactly(1)); - mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.exactly(1)); - mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2)); - mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1)); - - should(children.length).equal(mockDatabaseContainerCache.databases[mockSubscription.id].length); - - for (let ix = 0; ix < mockDatabaseContainerCache.databases[mockSubscription.id].length; ix++) { - should(children[ix].nodePathValue).equal(`database_${mockDatabaseContainerCache.databases[mockSubscription.id][ix].name}`); - } - }); - - it('Should handle when there is no databases.', async function(): Promise { - mockDatabaseService.setup((o) => o.getDatabases(mockSubscription, mockCredentials)).returns(() => Promise.resolve(undefined)); - - const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - - const children = await databaseContainerTreeNode.getChildren(); - - should(children).Array(); - should(children.length).equal(1); - should(children[0]).instanceof(AzureResourceMessageTreeNode); - should(children[0].nodePathValue).startWith('message_'); - should(children[0].getNodeInfo().label).equal('No SQL Databases found.'); - }); - - it('Should handle errors.', async function(): Promise { - const mockError = 'Test error'; - mockDatabaseService.setup((o) => o.getDatabases(mockSubscription, mockCredentials)).returns(() => { throw new Error(mockError); }); - - const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - const children = await databaseContainerTreeNode.getChildren(); - - mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once()); - mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.once()); - mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never()); - mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - - should(children).Array(); - should(children.length).equal(1); - should(children[0]).instanceof(AzureResourceMessageTreeNode); - should(children[0].nodePathValue).startWith('message_'); - should(children[0].getNodeInfo().label).equal(`Error: ${mockError}`); - }); -}); - -describe('AzureResourceDatabaseContainerTreeNode.clearCache', function() : void { - beforeEach(() => { - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - }); - - it('Should clear cache.', async function(): Promise { - const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - databaseContainerTreeNode.clearCache(); - should(databaseContainerTreeNode.isClearingCache).true(); - }); -}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts deleted file mode 100644 index 8ac8102a76..0000000000 --- a/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts +++ /dev/null @@ -1,219 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as should from 'should'; -import * as TypeMoq from 'typemoq'; -import * as sqlops from 'sqlops'; -import * as vscode from 'vscode'; -import 'mocha'; -import { ServiceClientCredentials } from 'ms-rest'; - -import { AzureResourceServicePool } from '../../../azureResource/servicePool'; -import { - IAzureResourceCacheService, - IAzureResourceContextService, - IAzureResourceCredentialService, - IAzureResourceDatabaseServerService -} from '../../../azureResource/interfaces'; -import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler'; -import { AzureResourceSubscription, AzureResourceDatabaseServer } from '../../../azureResource/models'; -import { AzureResourceItemType } from '../../../azureResource/constants'; -import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode'; -import { AzureResourceDatabaseServerContainerTreeNode } from '../../../azureResource/tree/databaseServerContainerTreeNode'; -import { AzureResourceDatabaseServerTreeNode } from '../../../azureResource/tree/databaseServerTreeNode'; - -// Mock services -const mockServicePool = AzureResourceServicePool.getInstance(); - -let mockCacheService: TypeMoq.IMock; -let mockContextService: TypeMoq.IMock; -let mockCredentialService: TypeMoq.IMock; -let mockDatabaseServerService: TypeMoq.IMock; - -let mockTreeChangeHandler: TypeMoq.IMock; - -// Mock test data -const mockAccount: sqlops.Account = { - key: { - accountId: 'mock_account', - providerId: 'mock_provider' - }, - displayInfo: { - displayName: 'mock_account@test.com', - accountType: 'Microsoft', - contextualDisplayName: 'test' - }, - properties: undefined, - isStale: false -}; - -const mockCredential = TypeMoq.Mock.ofType().object; -const mockCredentials = [mockCredential]; - -const mockSubscription: AzureResourceSubscription = { - id: 'mock_subscription', - name: 'mock subscription' -}; - -const mockDatabaseServer1: AzureResourceDatabaseServer = { - name: 'mock server 1', - fullName: 'mock server 1', - loginName: 'mock user 1', - defaultDatabaseName: 'master' -}; -const mockDatabaseServer2: AzureResourceDatabaseServer = { - name: 'mock server 2', - fullName: 'mock server 2', - loginName: 'mock user 2', - defaultDatabaseName: 'master' -}; -const mockDatabaseServers = [mockDatabaseServer1, mockDatabaseServer2]; - -let mockDatabaseServerContainerCache: { databaseServers: { [subscriptionId: string]: AzureResourceDatabaseServer[] } }; - -describe('AzureResourceDatabaseServerContainerTreeNode.info', function(): void { - beforeEach(() => { - mockContextService = TypeMoq.Mock.ofType(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockServicePool.contextService = mockContextService.object; - }); - - it('Should be correct when created.', async function(): Promise { - const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - - const databaseServerContainerTreeNodeLabel = 'SQL Servers'; - - should(databaseServerContainerTreeNode.nodePathValue).equal('databaseServerContainer'); - - const treeItem = await databaseServerContainerTreeNode.getTreeItem(); - should(treeItem.label).equal(databaseServerContainerTreeNodeLabel); - should(treeItem.contextValue).equal(AzureResourceItemType.databaseServerContainer); - should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); - - const nodeInfo = databaseServerContainerTreeNode.getNodeInfo(); - should(nodeInfo.isLeaf).false(); - should(nodeInfo.label).equal(databaseServerContainerTreeNodeLabel); - should(nodeInfo.nodeType).equal(AzureResourceItemType.databaseServerContainer); - should(nodeInfo.iconType).equal(AzureResourceItemType.databaseServerContainer); - }); -}); - -describe('AzureResourceDatabaseServerContainerTreeNode.getChildren', function(): void { - beforeEach(() => { - mockCacheService = TypeMoq.Mock.ofType(); - mockCredentialService = TypeMoq.Mock.ofType(); - mockDatabaseServerService = TypeMoq.Mock.ofType(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockDatabaseServerContainerCache = { databaseServers: {} }; - - mockServicePool.cacheService = mockCacheService.object; - mockServicePool.credentialService = mockCredentialService.object; - mockServicePool.databaseServerService = mockDatabaseServerService.object; - - mockCredentialService.setup((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockCredentials)); - mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockDatabaseServerContainerCache); - mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockDatabaseServerContainerCache.databaseServers[mockSubscription.id] = mockDatabaseServers); - }); - - it('Should load database servers from scratch and update cache when it is clearing cache.', async function(): Promise { - mockDatabaseServerService.setup((o) => o.getDatabaseServers(mockSubscription, mockCredentials)).returns(() => Promise.resolve(mockDatabaseServers)); - - const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - - const children = await databaseServerContainerTreeNode.getChildren(); - - mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once()); - mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.once()); - mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); - mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); - - should(databaseServerContainerTreeNode.isClearingCache).false(); - - should(children).Array(); - should(children.length).equal(mockDatabaseServers.length); - - should(Object.keys(mockDatabaseServerContainerCache.databaseServers)).deepEqual([mockSubscription.id]); - should(mockDatabaseServerContainerCache.databaseServers[mockSubscription.id]).deepEqual(mockDatabaseServers); - - for (let ix = 0; ix < mockDatabaseServers.length; ix++) { - const child = children[ix]; - const databaseServer = mockDatabaseServers[ix]; - - should(child).instanceof(AzureResourceDatabaseServerTreeNode); - should(child.nodePathValue).equal(`databaseServer_${databaseServer.name}`); - } - }); - - it('Should load database servers from cache when it is not clearing cache.', async function(): Promise { - mockDatabaseServerService.setup((o) => o.getDatabaseServers(mockSubscription, mockCredentials)).returns(() => Promise.resolve(mockDatabaseServers)); - - const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - - await databaseServerContainerTreeNode.getChildren(); - const children = await databaseServerContainerTreeNode.getChildren(); - - mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.exactly(1)); - mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.exactly(1)); - mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2)); - mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1)); - - should(children.length).equal(mockDatabaseServerContainerCache.databaseServers[mockSubscription.id].length); - - for (let ix = 0; ix < mockDatabaseServerContainerCache.databaseServers[mockSubscription.id].length; ix++) { - should(children[ix].nodePathValue).equal(`databaseServer_${mockDatabaseServerContainerCache.databaseServers[mockSubscription.id][ix].name}`); - } - }); - - it('Should handle when there is no database servers.', async function(): Promise { - mockDatabaseServerService.setup((o) => o.getDatabaseServers(mockSubscription, mockCredentials)).returns(() => Promise.resolve(undefined)); - - const databaseContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - - const children = await databaseContainerTreeNode.getChildren(); - - should(children).Array(); - should(children.length).equal(1); - should(children[0]).instanceof(AzureResourceMessageTreeNode); - should(children[0].nodePathValue).startWith('message_'); - should(children[0].getNodeInfo().label).equal('No SQL Servers found.'); - }); - - it('Should handle errors.', async function(): Promise { - const mockError = 'Test error'; - mockDatabaseServerService.setup((o) => o.getDatabaseServers(mockSubscription, mockCredentials)).returns(() => { throw new Error(mockError); }); - - const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - const children = await databaseServerContainerTreeNode.getChildren(); - - mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once()); - mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.once()); - mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never()); - mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - - should(children).Array(); - should(children.length).equal(1); - should(children[0]).instanceof(AzureResourceMessageTreeNode); - should(children[0].nodePathValue).startWith('message_'); - should(children[0].getNodeInfo().label).equal(`Error: ${mockError}`); - }); -}); - -describe('AzureResourceDatabaseServerContainerTreeNode.clearCache', function() : void { - beforeEach(() => { - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - }); - - it('Should clear cache.', async function(): Promise { - const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - databaseServerContainerTreeNode.clearCache(); - should(databaseServerContainerTreeNode.isClearingCache).true(); - }); -}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts deleted file mode 100644 index cef163b670..0000000000 --- a/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as should from 'should'; -import * as TypeMoq from 'typemoq'; -import * as vscode from 'vscode'; -import 'mocha'; - -import { AzureResourceServicePool } from '../../../azureResource/servicePool'; -import { IAzureResourceContextService } from '../../../azureResource/interfaces'; -import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler'; -import { AzureResourceDatabaseServer } from '../../../azureResource/models'; -import { AzureResourceItemType } from '../../../azureResource/constants'; -import { AzureResourceDatabaseServerTreeNode } from '../../../azureResource/tree/databaseServerTreeNode'; - -// Mock services -const mockServicePool = AzureResourceServicePool.getInstance(); - -let mockContextService: TypeMoq.IMock; - -let mockTreeChangeHandler: TypeMoq.IMock; - -// Mock test data -const mockDatabaseServer: AzureResourceDatabaseServer = { - name: 'mock database 1', - fullName: 'mock server 1', - loginName: 'mock user 1', - defaultDatabaseName: 'master' -}; - -describe('AzureResourceDatabaseServerTreeNode.info', function(): void { - beforeEach(() => { - mockContextService = TypeMoq.Mock.ofType(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockServicePool.contextService = mockContextService.object; - }); - - it('Should be correct when created.', async function(): Promise { - const databaseServerTreeNode = new AzureResourceDatabaseServerTreeNode(mockDatabaseServer, mockTreeChangeHandler.object, undefined); - - const databaseServerTreeNodeLabel = mockDatabaseServer.name; - - should(databaseServerTreeNode.nodePathValue).equal(`databaseServer_${mockDatabaseServer.name}`); - - const treeItem = await databaseServerTreeNode.getTreeItem(); - should(treeItem.label).equal(databaseServerTreeNodeLabel); - should(treeItem.contextValue).equal(AzureResourceItemType.databaseServer); - should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None); - - const nodeInfo = databaseServerTreeNode.getNodeInfo(); - should(nodeInfo.isLeaf).true(); - should(nodeInfo.label).equal(databaseServerTreeNodeLabel); - should(nodeInfo.nodeType).equal(AzureResourceItemType.databaseServer); - should(nodeInfo.iconType).equal(AzureResourceItemType.databaseServer); - }); -}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts deleted file mode 100644 index a1350771b8..0000000000 --- a/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as should from 'should'; -import * as TypeMoq from 'typemoq'; -import * as vscode from 'vscode'; -import 'mocha'; - -import { AzureResourceServicePool } from '../../../azureResource/servicePool'; -import { IAzureResourceContextService } from '../../../azureResource/interfaces'; -import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler'; -import { AzureResourceDatabase } from '../../../azureResource/models'; -import { AzureResourceItemType } from '../../../azureResource/constants'; -import { AzureResourceDatabaseTreeNode } from '../../../azureResource/tree/databaseTreeNode'; - -// Mock services -const mockServicePool = AzureResourceServicePool.getInstance(); - -let mockContextService: TypeMoq.IMock; - -let mockTreeChangeHandler: TypeMoq.IMock; - -// Mock test data -const mockDatabase: AzureResourceDatabase = { - name: 'mock database 1', - serverName: 'mock server 1', - serverFullName: 'mock server 1', - loginName: 'mock user 1' -}; - -describe('AzureResourceDatabaseTreeNode.info', function(): void { - beforeEach(() => { - mockContextService = TypeMoq.Mock.ofType(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockServicePool.contextService = mockContextService.object; - }); - - it('Should be correct.', async function(): Promise { - const databaseTreeNode = new AzureResourceDatabaseTreeNode(mockDatabase, mockTreeChangeHandler.object, undefined); - - const databaseTreeNodeLabel = `${mockDatabase.name} (${mockDatabase.serverName})`; - - should(databaseTreeNode.nodePathValue).equal(`database_${mockDatabase.name}`); - - const treeItem = await databaseTreeNode.getTreeItem(); - should(treeItem.label).equal(databaseTreeNodeLabel); - should(treeItem.contextValue).equal(AzureResourceItemType.database); - should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None); - - const nodeInfo = databaseTreeNode.getNodeInfo(); - should(nodeInfo.isLeaf).true(); - should(nodeInfo.label).equal(databaseTreeNodeLabel); - should(nodeInfo.nodeType).equal(AzureResourceItemType.database); - should(nodeInfo.iconType).equal(AzureResourceItemType.database); - }); -}); diff --git a/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts index e55621dfed..66ddf5b486 100644 --- a/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts +++ b/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts @@ -10,20 +10,24 @@ import * as TypeMoq from 'typemoq'; import * as sqlops from 'sqlops'; import * as vscode from 'vscode'; import 'mocha'; +import { AppContext } from '../../../appContext'; +import { ApiWrapper } from '../../../apiWrapper'; -import { AzureResourceServicePool } from '../../../azureResource/servicePool'; -import { IAzureResourceContextService } from '../../../azureResource/interfaces'; +import { azureResource } from '../../../azureResource/azure-resource'; import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler'; -import { AzureResourceSubscription } from '../../../azureResource/models'; import { AzureResourceSubscriptionTreeNode } from '../../../azureResource/tree/subscriptionTreeNode'; -import { AzureResourceDatabaseContainerTreeNode } from '../../../azureResource/tree/databaseContainerTreeNode'; -import { AzureResourceDatabaseServerContainerTreeNode } from '../../../azureResource/tree/databaseServerContainerTreeNode'; -import { AzureResourceItemType } from '../../../azureResource/constants'; +import { AzureResourceItemType, AzureResourceServiceNames } from '../../../azureResource/constants'; +import { AzureResourceService } from '../../../azureResource/resourceService'; +import { AzureResourceResourceTreeNode } from '../../../azureResource/resourceTreeNode'; +import { IAzureResourceCacheService } from '../../../azureResource/interfaces'; +import { generateGuid } from '../../../azureResource/utils'; // Mock services -const mockServicePool = AzureResourceServicePool.getInstance(); +let mockAppContext: AppContext; -let mockContextService: TypeMoq.IMock; +let mockExtensionContext: TypeMoq.IMock; +let mockApiWrapper: TypeMoq.IMock; +let mockCacheService: TypeMoq.IMock; let mockTreeChangeHandler: TypeMoq.IMock; @@ -42,24 +46,60 @@ const mockAccount: sqlops.Account = { isStale: false }; -const mockSubscription: AzureResourceSubscription = { +const mockSubscription: azureResource.AzureResourceSubscription = { id: 'mock_subscription', name: 'mock subscription' }; +const mockTenantId: string = 'mock_tenant'; + +let mockResourceTreeDataProvider1: TypeMoq.IMock; +let mockResourceProvider1: TypeMoq.IMock; + +let mockResourceTreeDataProvider2: TypeMoq.IMock; +let mockResourceProvider2: TypeMoq.IMock; + +const resourceService: AzureResourceService = AzureResourceService.getInstance(); + describe('AzureResourceSubscriptionTreeNode.info', function(): void { beforeEach(() => { - mockContextService = TypeMoq.Mock.ofType(); + mockExtensionContext = TypeMoq.Mock.ofType(); + mockApiWrapper = TypeMoq.Mock.ofType(); + mockCacheService = TypeMoq.Mock.ofType(); + + mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object); + + mockAppContext.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object); + + mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid()); mockTreeChangeHandler = TypeMoq.Mock.ofType(); - mockServicePool.contextService = mockContextService.object; + mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType(); + mockResourceTreeDataProvider1.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType().object])); + mockResourceTreeDataProvider1.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny())); + mockResourceProvider1 = TypeMoq.Mock.ofType(); + mockResourceProvider1.setup((o) => o.providerId).returns(() => 'mockResourceProvider1'); + mockResourceProvider1.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider1.object); + + mockResourceTreeDataProvider2 = TypeMoq.Mock.ofType(); + mockResourceTreeDataProvider2.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType().object])); + mockResourceTreeDataProvider2.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny())); + mockResourceProvider2 = TypeMoq.Mock.ofType(); + mockResourceProvider2.setup((o) => o.providerId).returns(() => 'mockResourceProvider2'); + mockResourceProvider2.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider2.object); + + resourceService.clearResourceProviders(); + resourceService.registerResourceProvider(mockResourceProvider1.object); + resourceService.registerResourceProvider(mockResourceProvider2.object); + + resourceService.areResourceProvidersLoaded = true; }); it('Should be correct when created.', async function(): Promise { - const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockAccount, mockSubscription, mockTenantId, mockAppContext, mockTreeChangeHandler.object, undefined); - should(subscriptionTreeNode.nodePathValue).equal(`subscription_${mockSubscription.id}`); + should(subscriptionTreeNode.nodePathValue).equal(`account_${mockAccount.key.accountId}.subscription_${mockSubscription.id}.tenant_${mockTenantId}`); const treeItem = await subscriptionTreeNode.getTreeItem(); should(treeItem.label).equal(mockSubscription.name); @@ -76,16 +116,52 @@ describe('AzureResourceSubscriptionTreeNode.info', function(): void { describe('AzureResourceSubscriptionTreeNode.getChildren', function(): void { beforeEach(() => { + mockExtensionContext = TypeMoq.Mock.ofType(); + mockApiWrapper = TypeMoq.Mock.ofType(); + mockCacheService = TypeMoq.Mock.ofType(); + + mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object); + mockAppContext.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object); + + mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid()); + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType(); + mockResourceTreeDataProvider1.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType().object])); + mockResourceTreeDataProvider1.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny())); + mockResourceProvider1 = TypeMoq.Mock.ofType(); + mockResourceProvider1.setup((o) => o.providerId).returns(() => 'mockResourceProvider1'); + mockResourceProvider1.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider1.object); + + mockResourceTreeDataProvider2 = TypeMoq.Mock.ofType(); + mockResourceTreeDataProvider2.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType().object])); + mockResourceTreeDataProvider2.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny())); + mockResourceProvider2 = TypeMoq.Mock.ofType(); + mockResourceProvider2.setup((o) => o.providerId).returns(() => 'mockResourceProvider2'); + mockResourceProvider2.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider2.object); + + resourceService.clearResourceProviders(); + resourceService.registerResourceProvider(mockResourceProvider1.object); + resourceService.registerResourceProvider(mockResourceProvider2.object); + + resourceService.areResourceProvidersLoaded = true; }); - it('Should load database containers.', async function(): Promise { - const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + it('Should return resource containers.', async function(): Promise { + const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockAccount, mockSubscription, mockTenantId, mockAppContext, mockTreeChangeHandler.object, undefined); const children = await subscriptionTreeNode.getChildren(); + mockResourceTreeDataProvider1.verify((o) => o.getChildren(), TypeMoq.Times.once()); + + mockResourceTreeDataProvider2.verify((o) => o.getChildren(), TypeMoq.Times.once()); + + const expectedChildren = await resourceService.listResourceProviderIds(); + should(children).Array(); - should(children.length).equal(2); - should(children[0]).instanceof(AzureResourceDatabaseContainerTreeNode); - should(children[1]).instanceof(AzureResourceDatabaseServerContainerTreeNode); + should(children.length).equal(expectedChildren.length); + for (const child of children) { + should(child).instanceOf(AzureResourceResourceTreeNode); + } }); }); diff --git a/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts b/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts index f8cf33c84c..520a1ab518 100644 --- a/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts +++ b/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts @@ -5,21 +5,28 @@ 'use strict'; +import * as vscode from 'vscode'; import * as should from 'should'; import * as TypeMoq from 'typemoq'; import * as sqlops from 'sqlops'; import 'mocha'; +import { AppContext } from '../../../appContext'; +import { ApiWrapper } from '../../../apiWrapper'; -import { AzureResourceServicePool } from '../../../azureResource/servicePool'; -import { IAzureResourceAccountService } from '../../../azureResource/interfaces'; +import { IAzureResourceCacheService, IAzureResourceAccountService } from '../../../azureResource/interfaces'; import { AzureResourceTreeProvider } from '../../../azureResource/tree/treeProvider'; import { AzureResourceAccountTreeNode } from '../../../azureResource/tree/accountTreeNode'; import { AzureResourceAccountNotSignedInTreeNode } from '../../../azureResource/tree/accountNotSignedInTreeNode'; -import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode'; +import { AzureResourceMessageTreeNode } from '../../../azureResource/messageTreeNode'; +import { AzureResourceServiceNames } from '../../../azureResource/constants'; +import { generateGuid } from '../../../azureResource/utils'; // Mock services -const mockServicePool = AzureResourceServicePool.getInstance(); +let mockAppContext: AppContext; +let mockExtensionContext: TypeMoq.IMock; +let mockApiWrapper: TypeMoq.IMock; +let mockCacheService: TypeMoq.IMock; let mockAccountService: TypeMoq.IMock; // Mock test data @@ -53,15 +60,23 @@ const mockAccounts = [mockAccount1, mockAccount2]; describe('AzureResourceTreeProvider.getChildren', function(): void { beforeEach(() => { + mockExtensionContext = TypeMoq.Mock.ofType(); + mockApiWrapper = TypeMoq.Mock.ofType(); + mockCacheService = TypeMoq.Mock.ofType(); mockAccountService = TypeMoq.Mock.ofType(); - mockServicePool.accountService = mockAccountService.object; + mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object); + + mockAppContext.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object); + mockAppContext.registerService(AzureResourceServiceNames.accountService, mockAccountService.object); + + mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid()); }); it('Should load accounts.', async function(): Promise { mockAccountService.setup((o) => o.getAccounts()).returns(() => Promise.resolve(mockAccounts)); - const treeProvider = new AzureResourceTreeProvider(); + const treeProvider = new AzureResourceTreeProvider(mockAppContext); treeProvider.isSystemInitialized = true; const children = await treeProvider.getChildren(undefined); @@ -83,7 +98,7 @@ describe('AzureResourceTreeProvider.getChildren', function(): void { it('Should handle when there is no accounts.', async function(): Promise { mockAccountService.setup((o) => o.getAccounts()).returns(() => Promise.resolve(undefined)); - const treeProvider = new AzureResourceTreeProvider(); + const treeProvider = new AzureResourceTreeProvider(mockAppContext); treeProvider.isSystemInitialized = true; const children = await treeProvider.getChildren(undefined); @@ -97,7 +112,7 @@ describe('AzureResourceTreeProvider.getChildren', function(): void { const mockAccountError = 'Test account error'; mockAccountService.setup((o) => o.getAccounts()).returns(() => { throw new Error(mockAccountError); }); - const treeProvider = new AzureResourceTreeProvider(); + const treeProvider = new AzureResourceTreeProvider(mockAppContext); treeProvider.isSystemInitialized = true; const children = await treeProvider.getChildren(undefined); diff --git a/extensions/azurecore/yarn.lock b/extensions/azurecore/yarn.lock index 5540b46559..ad88fb28d0 100644 --- a/extensions/azurecore/yarn.lock +++ b/extensions/azurecore/yarn.lock @@ -5,22 +5,18 @@ "@types/mocha@^5.2.5": version "5.2.5" resolved "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz#8a4accfc403c124a0bafe8a9fc61a05ec1032073" - integrity sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww== "@types/node@^8.0.24": version "8.10.36" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.36.tgz#eac05d576fbcd0b4ea3c912dc58c20475c08d9e4" - integrity sha512-SL6KhfM7PTqiFmbCW3eVNwVBZ+88Mrzbuvn9olPsfv43mbiWaFY+nRcz/TGGku0/lc2FepdMbImdMY1JrQ+zbw== "@types/node@^8.0.47": version "8.10.30" resolved "https://registry.npmjs.org/@types/node/-/node-8.10.30.tgz#2c82cbed5f79d72280c131d2acffa88fbd8dd353" - integrity sha512-Le8HGMI5gjFSBqcCuKP/wfHC19oURzkU2D+ERIescUoJd+CmNEMYBib9LQ4zj1HHEZOJQWhw2ZTnbD8weASh/Q== adal-node@^0.1.28: version "0.1.28" resolved "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz#468c4bb3ebbd96b1270669f4b9cba4e0065ea485" - integrity sha1-RoxLs+u9lrEnBmn0ucuk4AZepIU= dependencies: "@types/node" "^8.0.47" async ">=0.6.0" @@ -35,58 +31,108 @@ adal-node@^0.1.28: ajv@^5.3.0: version "5.5.2" resolved "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" - integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= dependencies: co "^4.6.0" fast-deep-equal "^1.0.0" fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" +ansi-cyan@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-cyan/-/ansi-cyan-0.1.1.tgz#538ae528af8982f28ae30d86f2f17456d2609873" + dependencies: + ansi-wrap "0.1.0" + +ansi-red@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-red/-/ansi-red-0.1.1.tgz#8c638f9d1080800a353c9c28c8a81ca4705d946c" + dependencies: + ansi-wrap "0.1.0" + +ansi-wrap@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" + +append-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/append-buffer/-/append-buffer-1.0.2.tgz#d8220cf466081525efea50614f3de6514dfa58f1" + dependencies: + buffer-equal "^1.0.0" + +arr-diff@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-1.1.0.tgz#687c32758163588fef7de7b36fabe495eb1a399a" + dependencies: + arr-flatten "^1.0.1" + array-slice "^0.2.3" + +arr-flatten@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +arr-union@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-2.1.0.tgz#20f9eab5ec70f5c7d215b1077b1c39161d292c7d" + +array-differ@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" + +array-slice@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + asn1@~0.2.3: version "0.2.4" resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== dependencies: safer-buffer "~2.1.0" assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= async@2.6.0: version "2.6.0" resolved "https://registry.npmjs.org/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" - integrity sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw== dependencies: lodash "^4.14.0" async@>=0.6.0: version "2.6.1" resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== dependencies: lodash "^4.17.10" asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: version "1.8.0" resolved "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" - integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== azure-arm-resource@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/azure-arm-resource/-/azure-arm-resource-7.0.0.tgz#e76fe2195abe354b607346c2fa0f690544176294" - integrity sha512-LW1OmW49d5xQo/KDBK2BNfoFVOlP8Gq9yKqP2kz0e6RURl5UXhIfN65Y4GeJramuyGIOXeGPV+NrXrzl1k4d4g== dependencies: ms-rest "^2.3.3" ms-rest-azure "^2.5.5" @@ -94,7 +140,6 @@ azure-arm-resource@^7.0.0: azure-arm-sql@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/azure-arm-sql/-/azure-arm-sql-5.0.1.tgz#75c0b115525d2270ab16122d47d0c666aca175d4" - integrity sha512-n+c1cfCnBCGL5lOdPtCHve0Meu3vclwGEJxvMBYC6e3CjiNrI0eoaPtS3Aug4M/srVJPCNOKCTyuLj0OIc7Aww== dependencies: ms-rest "^2.3.3" ms-rest-azure "^2.5.5" @@ -102,115 +147,184 @@ azure-arm-sql@^5.0.1: balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= dependencies: tweetnacl "^0.14.3" +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" +browser-stdout@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" + browser-stdout@1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + +buffer-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" caseless@~0.12.0: version "0.12.0" resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= circular-json@^0.3.1: version "0.3.3" resolved "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" - integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== + +clone-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" + +clone-stats@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" + +clone-stats@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" + +clone@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/clone/-/clone-0.2.0.tgz#c6126a90ad4f72dbf5acdb243cc37724fe93fc1f" + +clone@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + +clone@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + +cloneable-readable@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.2.tgz#d591dee4a8f8bc15da43ce97dceeba13d43e2a65" + dependencies: + inherits "^2.0.1" + process-nextick-args "^2.0.0" + readable-stream "^2.3.5" co@^4.6.0: version "4.6.0" resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= combined-stream@1.0.6: version "1.0.6" resolved "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" - integrity sha1-cj599ugBrFYTETp+RFqbactjKBg= dependencies: delayed-stream "~1.0.0" combined-stream@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" - integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== dependencies: delayed-stream "~1.0.0" +commander@2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" + commander@2.15.1: version "2.15.1" resolved "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" - integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -core-util-is@1.0.2: +convert-source-map@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" + dependencies: + safe-buffer "~5.1.1" + +core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= dashdash@^1.12.0: version "1.14.1" resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= dependencies: assert-plus "^1.0.0" date-utils@*: version "1.2.21" resolved "https://registry.npmjs.org/date-utils/-/date-utils-1.2.21.tgz#61fb16cdc1274b3c9acaaffe9fc69df8720a2b64" - integrity sha1-YfsWzcEnSzyayq/+n8ad+HIKK2Q= debug@3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== dependencies: ms "2.0.0" +deep-assign@^1.0.0: + version "1.0.0" + resolved "http://registry.npmjs.org/deep-assign/-/deep-assign-1.0.0.tgz#b092743be8427dc621ea0067cdec7e70dd19f37b" + dependencies: + is-obj "^1.0.0" + +define-properties@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + dependencies: + object-keys "^1.0.12" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +diff@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" diff@3.5.0: version "3.5.0" resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== -duplexer@^0.1.1: +duplexer@^0.1.1, duplexer@~0.1.1: version "0.1.1" resolved "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" - integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= + +duplexify@^3.6.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.1.tgz#b1a7a29c4abfd639585efaecce80d666b1e34125" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= dependencies: jsbn "~0.1.0" safer-buffer "^2.1.0" @@ -218,70 +332,153 @@ ecc-jsbn@~0.1.1: ecdsa-sig-formatter@1.0.10: version "1.0.10" resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" - integrity sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM= dependencies: safe-buffer "^5.0.1" +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + dependencies: + once "^1.4.0" + escape-string-regexp@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -extend@~3.0.2: +event-stream@3.3.4: + version "3.3.4" + resolved "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" + dependencies: + duplexer "~0.1.1" + from "~0" + map-stream "~0.1.0" + pause-stream "0.0.11" + split "0.3" + stream-combiner "~0.0.4" + through "~2.3.1" + +event-stream@~3.3.4: + version "3.3.5" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.5.tgz#e5dd8989543630d94c6cf4d657120341fa31636b" + dependencies: + duplexer "^0.1.1" + from "^0.1.7" + map-stream "0.0.7" + pause-stream "^0.0.11" + split "^1.0.1" + stream-combiner "^0.2.2" + through "^2.3.8" + +extend-shallow@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-1.1.4.tgz#19d6bf94dfc09d76ba711f39b872d21ff4dd9071" + dependencies: + kind-of "^1.1.0" + +extend@^3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== extsprintf@1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= extsprintf@^1.2.0: version "1.4.0" resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" - integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + dependencies: + pend "~1.2.0" + +flush-write-stream@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.4" forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= form-data@~2.3.2: version "2.3.2" resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" - integrity sha1-SXBJi+YEwgwAXU9cI67NIda0kJk= dependencies: asynckit "^0.4.0" combined-stream "1.0.6" mime-types "^2.1.12" +from@^0.1.7, from@~0: + version "0.1.7" + resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" + +fs-mkdirp-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz#0b7815fc3201c6a69e14db98ce098c16935259eb" + dependencies: + graceful-fs "^4.1.11" + through2 "^2.0.3" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fstream@^1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" getpass@^0.1.1: version "0.1.7" resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= dependencies: assert-plus "^1.0.0" +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-stream@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4" + dependencies: + extend "^3.0.0" + glob "^7.1.1" + glob-parent "^3.1.0" + is-negated-glob "^1.0.0" + ordered-read-streams "^1.0.0" + pumpify "^1.3.5" + readable-stream "^2.1.5" + remove-trailing-separator "^1.0.1" + to-absolute-glob "^2.0.0" + unique-stream "^2.0.2" + glob@7.1.2: version "7.1.2" resolved "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -290,38 +487,120 @@ glob@7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.1.15" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" + +growl@1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" + growl@1.10.5: version "1.10.5" resolved "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +gulp-chmod@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/gulp-chmod/-/gulp-chmod-2.0.0.tgz#00c390b928a0799b251accf631aa09e01cc6299c" + dependencies: + deep-assign "^1.0.0" + stat-mode "^0.2.0" + through2 "^2.0.0" + +gulp-filter@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/gulp-filter/-/gulp-filter-5.1.0.tgz#a05e11affb07cf7dcf41a7de1cb7b63ac3783e73" + dependencies: + multimatch "^2.0.0" + plugin-error "^0.1.2" + streamfilter "^1.0.5" + +gulp-gunzip@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gulp-gunzip/-/gulp-gunzip-1.0.0.tgz#15b741145e83a9c6f50886241b57cc5871f151a9" + dependencies: + through2 "~0.6.5" + vinyl "~0.4.6" + +gulp-remote-src-vscode@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/gulp-remote-src-vscode/-/gulp-remote-src-vscode-0.5.1.tgz#a528509457affff3ff30cc73a4a97afe31c41c1d" + dependencies: + event-stream "3.3.4" + node.extend "^1.1.2" + request "^2.79.0" + through2 "^2.0.3" + vinyl "^2.0.1" + +gulp-untar@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/gulp-untar/-/gulp-untar-0.0.7.tgz#92067d79e0fa1e92d60562a100233a44a5aa08b4" + dependencies: + event-stream "~3.3.4" + streamifier "~0.1.1" + tar "^2.2.1" + through2 "~2.0.3" + vinyl "^1.2.0" + +gulp-vinyl-zip@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/gulp-vinyl-zip/-/gulp-vinyl-zip-2.1.2.tgz#b79cc1a0e2c3b158ffee294590ade1e9caaf5e7b" + dependencies: + event-stream "3.3.4" + queue "^4.2.1" + through2 "^2.0.3" + vinyl "^2.0.2" + vinyl-fs "^3.0.3" + yauzl "^2.2.1" + yazl "^2.2.1" har-schema@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= har-validator@~5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" - integrity sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA== dependencies: ajv "^5.3.0" har-schema "^2.0.0" +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + dependencies: + function-bind "^1.1.1" he@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" - integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= http-signature@~1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= dependencies: assert-plus "^1.0.0" jsprim "^1.2.2" @@ -330,60 +609,120 @@ http-signature@~1.2.0: inflight@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= dependencies: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -is-buffer@^1.1.6: +is-absolute@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" + dependencies: + is-relative "^1.0.0" + is-windows "^1.0.1" + +is-buffer@^1.1.5, is-buffer@^1.1.6: version "1.1.6" resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-extglob@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + dependencies: + is-extglob "^2.1.0" + +is-negated-glob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" + +is-obj@^1.0.0: + version "1.0.1" + resolved "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + +is-relative@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" + dependencies: + is-unc-path "^1.0.0" is-stream@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-unc-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" + dependencies: + unc-path-regex "^0.1.2" + +is-utf8@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +is-valid-glob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" + +is-windows@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + +is@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is/-/is-3.2.1.tgz#d0ac2ad55eb7b0bec926a5266f6c662aaa83dca5" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= jsbn@~0.1.0: version "0.1.1" resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= json-schema-traverse@^0.3.0: version "0.3.1" resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" - integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= json-schema@0.2.3: version "0.2.3" resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stable-stringify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" jsprim@^1.2.2: version "1.4.1" resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= dependencies: assert-plus "1.0.0" extsprintf "1.3.0" @@ -393,7 +732,6 @@ jsprim@^1.2.2: jwa@^1.1.5: version "1.1.6" resolved "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" - integrity sha512-tBO/cf++BUsJkYql/kBbJroKOgHWEigTKBAjjBEmrMGYd1QMBC74Hr4Wo2zCZw6ZrVhlJPvoMrkcOnlWR/DJfw== dependencies: buffer-equal-constant-time "1.0.1" ecdsa-sig-formatter "1.0.10" @@ -402,51 +740,82 @@ jwa@^1.1.5: jws@3.x.x: version "3.1.5" resolved "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" - integrity sha512-GsCSexFADNQUr8T5HPJvayTjvPIfoyJPtLQBwn5a4WZQchcrPMPMAWcC1AzJVRDKyD6ZPROPAxgv6rfHViO4uQ== dependencies: jwa "^1.1.5" safe-buffer "^5.0.1" +kind-of@^1.1.0: + version "1.1.0" + resolved "http://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz#140a3d2d41a36d2efcfa9377b62c24f8495a5c44" + +lazystream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" + dependencies: + readable-stream "^2.0.5" + +lead@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42" + dependencies: + flush-write-stream "^1.0.2" + lodash@^4.14.0, lodash@^4.17.10, lodash@^4.17.4: version "4.17.11" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +map-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" + +map-stream@~0.1.0: + version "0.1.0" + resolved "http://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" mime-db@~1.36.0: version "1.36.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" - integrity sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw== mime-types@^2.1.12, mime-types@~2.1.19: version "2.1.20" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" - integrity sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A== dependencies: mime-db "~1.36.0" -minimatch@3.0.4, minimatch@^3.0.4: +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" minimist@0.0.8: version "0.0.8" resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -mkdirp@0.5.1: +mkdirp@0.5.1, "mkdirp@>=0.5 0": version "0.5.1" resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= dependencies: minimist "0.0.8" +mocha@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794" + dependencies: + browser-stdout "1.3.0" + commander "2.11.0" + debug "3.1.0" + diff "3.3.1" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.3" + he "1.1.1" + mkdirp "0.5.1" + supports-color "4.4.0" + mocha@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" - integrity sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ== dependencies: browser-stdout "1.3.1" commander "2.15.1" @@ -463,12 +832,10 @@ mocha@^5.2.0: moment@^2.21.0, moment@^2.22.2: version "2.22.2" resolved "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" - integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= ms-rest-azure@^2.5.5: version "2.5.9" resolved "https://registry.npmjs.org/ms-rest-azure/-/ms-rest-azure-2.5.9.tgz#8599943e349c91eb367d2d1dcb885017518dc712" - integrity sha512-qonobzWLS7Jl6qwgTuA/SfyCtnv7olvCRKrcF8nzXSj68ds4Oj3K64ntzgQajroKa0hKVMcPUFbTk1IYMGvu8w== dependencies: adal-node "^0.1.28" async "2.6.0" @@ -480,7 +847,6 @@ ms-rest-azure@^2.5.5: ms-rest@^2.3.2, ms-rest@^2.3.3: version "2.3.7" resolved "https://registry.npmjs.org/ms-rest/-/ms-rest-2.3.7.tgz#8bfc82fb91807643fcaa487c5fc9698cd18a018c" - integrity sha512-zZwuckC/Uv8F1Jr1bW+U1tsDTErWhtH6W4mpxvRrta4YrKwkFeLMt53RsaDOWTqMFsVpjNuCfznV1uxeGUF3/g== dependencies: duplexer "^0.1.1" is-buffer "^1.1.6" @@ -494,54 +860,192 @@ ms-rest@^2.3.2, ms-rest@^2.3.3: ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +multimatch@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b" + dependencies: + array-differ "^1.0.0" + array-union "^1.0.1" + arrify "^1.0.0" + minimatch "^3.0.0" + +node.extend@^1.1.2: + version "1.1.8" + resolved "https://registry.yarnpkg.com/node.extend/-/node.extend-1.1.8.tgz#0aab3e63789f4e6d68b42bc00073ad1881243cf0" + dependencies: + has "^1.0.3" + is "^3.2.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +now-and-later@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.0.tgz#bc61cbb456d79cb32207ce47ca05136ff2e7d6ee" + dependencies: + once "^1.3.2" oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -once@^1.3.0: +object-keys@^1.0.11, object-keys@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" + +object.assign@^4.0.4: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" +ordered-read-streams@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" + dependencies: + readable-stream "^2.0.1" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +pause-stream@0.0.11, pause-stream@^0.0.11: + version "0.0.11" + resolved "http://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + dependencies: + through "~2.3" + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" performance-now@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +plugin-error@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace" + dependencies: + ansi-cyan "^0.1.1" + ansi-red "^0.1.1" + arr-diff "^1.0.1" + arr-union "^2.0.1" + extend-shallow "^1.1.2" postinstall-build@^5.0.1: version "5.0.3" resolved "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7" - integrity sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg== + +process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" psl@^1.1.24: version "1.1.29" resolved "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" - integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ== + +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.5: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" punycode@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= qs@~6.5.2: version "6.5.2" resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== -request@2.88.0, "request@>= 2.52.0", request@^2.88.0: +querystringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef" + +queue@^4.2.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/queue/-/queue-4.5.1.tgz#6e4290a2d7e99dc75b34494431633fe5437b0dac" + dependencies: + inherits "~2.0.0" + +"readable-stream@>=1.0.33-1 <1.1.0-0": + version "1.0.34" + resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@~2.3.6: + version "2.3.6" + resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +remove-bom-buffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53" + dependencies: + is-buffer "^1.1.5" + is-utf8 "^0.2.1" + +remove-bom-stream@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz#05f1a593f16e42e1fb90ebf59de8e569525f9523" + dependencies: + remove-bom-buffer "^3.0.0" + safe-buffer "^5.1.0" + through2 "^2.0.3" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + +replace-ext@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" + +replace-ext@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" + +request@2.88.0, "request@>= 2.52.0", request@^2.79.0, request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -564,27 +1068,43 @@ request@2.88.0, "request@>= 2.52.0", request@^2.88.0: tunnel-agent "^0.6.0" uuid "^3.3.2" -safe-buffer@^5.0.1, safe-buffer@^5.1.2: +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + +resolve-options@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131" + dependencies: + value-or-function "^3.0.0" + +rimraf@2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^5.4.1: + version "5.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" should-equal@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" - integrity sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA== dependencies: should-type "^1.4.0" should-format@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1" - integrity sha1-m/yPdPo5IFxT04w01xcwPidxJPE= dependencies: should-type "^1.3.0" should-type-adaptors "^1.0.1" @@ -592,7 +1112,6 @@ should-format@^3.0.3: should-type-adaptors@^1.0.1: version "1.1.0" resolved "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a" - integrity sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA== dependencies: should-type "^1.3.0" should-util "^1.0.0" @@ -600,17 +1119,14 @@ should-type-adaptors@^1.0.1: should-type@^1.3.0, should-type@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3" - integrity sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM= should-util@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz#c98cda374aa6b190df8ba87c9889c2b4db620063" - integrity sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM= should@^13.2.1: version "13.2.3" resolved "https://registry.npmjs.org/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10" - integrity sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ== dependencies: should-equal "^2.0.0" should-format "^3.0.3" @@ -618,10 +1134,32 @@ should@^13.2.1: should-type-adaptors "^1.0.1" should-util "^1.0.0" +source-map-support@^0.5.0: + version "0.5.9" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +split@0.3: + version "0.3.3" + resolved "http://registry.npmjs.org/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" + dependencies: + through "2" + +split@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + dependencies: + through "2" + sshpk@^1.7.0: version "1.14.2" resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98" - integrity sha1-xvxhZIo9nE52T9P8306hBeSSupg= dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -634,22 +1172,108 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" +stat-mode@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-0.2.2.tgz#e6c80b623123d7d80cf132ce538f346289072502" + +stream-combiner@^0.2.2: + version "0.2.2" + resolved "http://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858" + dependencies: + duplexer "~0.1.1" + through "~2.3.4" + +stream-combiner@~0.0.4: + version "0.0.4" + resolved "http://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" + dependencies: + duplexer "~0.1.1" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +streamfilter@^1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/streamfilter/-/streamfilter-1.0.7.tgz#ae3e64522aa5a35c061fd17f67620c7653c643c9" + dependencies: + readable-stream "^2.0.2" + +streamifier@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/streamifier/-/streamifier-0.1.1.tgz#97e98d8fa4d105d62a2691d1dc07e820db8dfc4f" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + +supports-color@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + dependencies: + has-flag "^2.0.0" + supports-color@5.4.0: version "5.4.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" - integrity sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w== dependencies: has-flag "^3.0.0" -through@^2.3.8: +tar@^2.2.1: + version "2.2.1" + resolved "http://registry.npmjs.org/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +through2-filter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-2.0.0.tgz#60bc55a0dacb76085db1f9dae99ab43f83d622ec" + dependencies: + through2 "~2.0.0" + xtend "~4.0.0" + +through2@^2.0.0, through2@^2.0.3, through2@~2.0.0, through2@~2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through2@~0.6.5: + version "0.6.5" + resolved "http://registry.npmjs.org/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" + dependencies: + readable-stream ">=1.0.33-1 <1.1.0-0" + xtend ">=4.0.0 <4.1.0-0" + +through@2, through@^2.3.8, through@~2.3, through@~2.3.1, through@~2.3.4: version "2.3.8" resolved "http://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +to-absolute-glob@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b" + dependencies: + is-absolute "^1.0.0" + is-negated-glob "^1.0.0" + +to-through@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-through/-/to-through-2.0.0.tgz#fc92adaba072647bc0b67d6b03664aa195093af6" + dependencies: + through2 "^2.0.3" tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== dependencies: psl "^1.1.24" punycode "^1.4.1" @@ -657,63 +1281,182 @@ tough-cookie@~2.4.3: tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= dependencies: safe-buffer "^5.0.1" tunnel@0.0.5: version "0.0.5" resolved "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz#d1532254749ed36620fcd1010865495a1fa9d0ae" - integrity sha512-gj5sdqherx4VZKMcBA4vewER7zdK25Td+z1npBqpbDys4eJrLx+SlYjJvq1bDXs2irkuJM5pf8ktaEQVipkrbA== tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= typemoq@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8" - integrity sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw== dependencies: circular-json "^0.3.1" lodash "^4.17.4" postinstall-build "^5.0.1" +unc-path-regex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + "underscore@>= 1.3.1": version "1.9.1" resolved "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" - integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== + +unique-stream@^2.0.2: + version "2.2.1" + resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.2.1.tgz#5aa003cfbe94c5ff866c4e7d668bb1c4dbadb369" + dependencies: + json-stable-stringify "^1.0.0" + through2-filter "^2.0.0" + +url-parse@^1.4.3: + version "1.4.4" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.4.tgz#cac1556e95faa0303691fec5cf9d5a1bc34648f8" + dependencies: + querystringify "^2.0.0" + requires-port "^1.0.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: version "3.3.2" resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +value-or-function@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813" verror@1.10.0: version "1.10.0" resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= dependencies: assert-plus "^1.0.0" core-util-is "1.0.2" extsprintf "^1.2.0" +vinyl-fs@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7" + dependencies: + fs-mkdirp-stream "^1.0.0" + glob-stream "^6.1.0" + graceful-fs "^4.0.0" + is-valid-glob "^1.0.0" + lazystream "^1.0.0" + lead "^1.0.0" + object.assign "^4.0.4" + pumpify "^1.3.5" + readable-stream "^2.3.3" + remove-bom-buffer "^3.0.0" + remove-bom-stream "^1.2.0" + resolve-options "^1.1.0" + through2 "^2.0.0" + to-through "^2.0.0" + value-or-function "^3.0.0" + vinyl "^2.0.0" + vinyl-sourcemap "^1.1.0" + +vinyl-source-stream@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vinyl-source-stream/-/vinyl-source-stream-1.1.2.tgz#62b53a135610a896e98ca96bee3a87f008a8e780" + dependencies: + through2 "^2.0.3" + vinyl "^0.4.3" + +vinyl-sourcemap@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz#92a800593a38703a8cdb11d8b300ad4be63b3e16" + dependencies: + append-buffer "^1.0.2" + convert-source-map "^1.5.0" + graceful-fs "^4.1.6" + normalize-path "^2.1.1" + now-and-later "^2.0.0" + remove-bom-buffer "^3.0.0" + vinyl "^2.0.0" + +vinyl@^0.4.3, vinyl@~0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" + dependencies: + clone "^0.2.0" + clone-stats "^0.0.1" + +vinyl@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884" + dependencies: + clone "^1.0.0" + clone-stats "^0.0.1" + replace-ext "0.0.1" + +vinyl@^2.0.0, vinyl@^2.0.1, vinyl@^2.0.2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86" + dependencies: + clone "^2.1.1" + clone-buffer "^1.0.0" + clone-stats "^1.0.0" + cloneable-readable "^1.0.0" + remove-trailing-separator "^1.0.1" + replace-ext "^1.0.0" + vscode-nls@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" - integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw== + +vscode@^1.1.26: + version "1.1.26" + resolved "https://registry.yarnpkg.com/vscode/-/vscode-1.1.26.tgz#33d0feacd8ab5f78a0c4672235376c70cdea494b" + dependencies: + glob "^7.1.2" + gulp-chmod "^2.0.0" + gulp-filter "^5.0.1" + gulp-gunzip "1.0.0" + gulp-remote-src-vscode "^0.5.1" + gulp-untar "^0.0.7" + gulp-vinyl-zip "^2.1.2" + mocha "^4.0.1" + request "^2.88.0" + semver "^5.4.1" + source-map-support "^0.5.0" + url-parse "^1.4.3" + vinyl-fs "^3.0.3" + vinyl-source-stream "^1.1.0" wrappy@1: version "1.0.2" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= "xmldom@>= 0.1.x": version "0.1.27" resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" - integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk= xpath.js@~1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1" + +"xtend@>=4.0.0 <4.1.0-0", xtend@~4.0.0, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +yauzl@^2.2.1: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + +yazl@^2.2.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35" + dependencies: + buffer-crc32 "~0.2.3" diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 4e613f5592..6e6b94d325 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -10,16 +10,16 @@ set VSCODEUSERDATADIR=%TMP%\vscodeuserfolder-%RANDOM%-%TIME:~6,5% :: call .\scripts\code.bat %~dp0\..\extensions\vscode-api-tests\testWorkspace --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% call .\scripts\code.bat %~dp0\..\extensions\vscode-colorize-tests\test --extensionDevelopmentPath=%~dp0\..\extensions\vscode-colorize-tests --extensionTestsPath=%~dp0\..\extensions\vscode-colorize-tests\out --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% call .\scripts\code.bat %~dp0\..\extensions\markdown-language-features\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\markdown-language-features --extensionTestsPath=%~dp0\..\extensions\markdown-language-features\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% -call .\scripts\code.bat %~dp0\..\extensions\azure\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\azure --extensionTestsPath=%~dp0\..\extensions\azure\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% +call .\scripts\code.bat %~dp0\..\extensions\azurecore\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\azurecore --extensionTestsPath=%~dp0\..\extensions\azurecore\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% if %errorlevel% neq 0 exit /b %errorlevel% -call .\scripts\code.bat $%~dp0\..\extensions\emmet\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% . -if %errorlevel% neq 0 exit /b %errorlevel% +:: call .\scripts\code.bat $%~dp0\..\extensions\emmet\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% . +:: if %errorlevel% neq 0 exit /b %errorlevel% :: Integration & performance tests in AMD call .\scripts\test.bat --runGlob **\*.integrationTest.js %* -# Tests in commonJS (HTML, CSS, JSON language server tests...) +:: Tests in commonJS (HTML, CSS, JSON language server tests...) call .\scripts\node-electron.bat .\node_modules\mocha\bin\_mocha .\extensions\*\server\out\test\**\*.test.js if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index fba91607f9..42a5ddf968 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -17,7 +17,7 @@ cd $ROOT # ./scripts/code.sh $ROOT/extensions/vscode-api-tests/testWorkspace --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started ./scripts/code.sh $ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started ./scripts/code.sh $ROOT/extensions/markdown-language-features/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started -./scripts/code.sh $ROOT/extensions/azure/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/azure --extensionTestsPath=$ROOT/extensions/azure/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started +./scripts/code.sh $ROOT/extensions/azurecore/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/azurecore --extensionTestsPath=$ROOT/extensions/azurecore/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started mkdir $ROOT/extensions/emmet/test-fixtures ./scripts/code.sh $ROOT/extensions/emmet/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started .