Enable long running tests and use new federated credentials for testing (#864)

* WIP: Get token credential from SP

* WIP: See is test provider can sign in

* WIP: Remove webConfig, dont import dev into production code

* WIP: Remove web entry point from package.json

* Add await to the logIn

* Add listing tests

* Fix up resource listing test

* Update service connection

* Hardcode long running tests

* Add some console.debug

* Make token static

* Test code

* Add default

* Add resource provider to global test

* Use the resource provider instead of trying to calling it directly

* Try using the key vault values

* Use fetch and see response type

* Dont parse response2

* wip

* Add node-fetch as a resource

* Make token code use function

* Use service client from @azure/core-client

* Final test using service client

* Remove unused context

* Confirm access token is being made

* Leverage auth package

* Add create and delete tests for Resource Groups

* Comments

* Import settingUtils from bundle

* Import LocationListStep from bundle?

* Reorganize and remove some inports from bundle

* Add hasPortalUrl

* See if there are any build errors

* use auth refactor

* update package json

* use newly released auth package

* use parameter

* remove installing azure account

* remove pretest step

* remove installing azure account fr fr

* remove from recommendations

* reintroduce gulp

* nuclear warfare

* add webpack to pretest

* logs

* try logging error

* use the new env vars with the underscore

* don't swallow errors

* cleanup

* only set to true when doing azure federated credentials

* use new branch for quick testing

* use main again

* refactor subscription provider logic

* remove commented out code

* set in the helper

* make optional

---------

Co-authored-by: Nathan Turinski <naturins@microsoft.com>
This commit is contained in:
Hossam Mabed 2024-05-24 14:14:58 -07:00 коммит произвёл GitHub
Родитель f05be6891e
Коммит e0560951b1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
14 изменённых файлов: 961 добавлений и 329 удалений

Просмотреть файл

@ -27,3 +27,5 @@ resources:
# Use those templates
extends:
template: azure-pipelines/1esmain.yml@azExtTemplates
parameters:
useAzureFederatedCredentials: true

1
.vscode/extensions.json поставляемый
Просмотреть файл

@ -1,6 +1,5 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"ms-vscode.azure-account"
]
}

Просмотреть файл

@ -14,6 +14,7 @@
//
// The tests should import '../extension.bundle'. At design-time they live in tests/ and so will pick up this file (extension.bundle.ts).
// At runtime the tests live in dist/tests and will therefore pick up the main webpack bundle at dist/extension.bundle.js.
export { LocationListStep } from '@microsoft/vscode-azext-azureutils';
export * from '@microsoft/vscode-azext-utils';
export * from './api/src/AzExtResourceType';
// export * from './api/src';
@ -23,25 +24,27 @@ export * from './api/src/resources/base';
export * from './api/src/resources/workspace';
export * from './api/src/utils/getApi';
export * from './api/src/utils/wrapper';
export * from './src/api/DefaultAzureResourceProvider';
export { convertV1TreeItemId } from './src/api/compatibility/CompatibleAzExtTreeDataProvider';
export * from './src/commands/openInPortal';
export { hasPortalUrl } from './src/commands/openInPortal';
export * from './src/commands/tags/getTagDiagnostics';
export * from './src/commands/viewProperties';
export * from './src/services/AzureResourcesService';
// Export activate/deactivate for main.js
export { createResourceGroup } from './src/commands/createResourceGroup';
export * from './src/commands/deleteResourceGroup/v2/deleteResourceGroupV2';
export { activate, deactivate } from './src/extension';
export * from './src/extensionVariables';
export * from './src/hostapi.v2.internal';
export * from './src/tree/BranchDataItemWrapper';
export * from './src/tree/InvalidItem';
export { ResourceGroupsItem } from './src/tree/ResourceGroupsItem';
export * from './src/tree/azure/AzureResourceItem';
export * from './src/tree/azure/SubscriptionItem';
export { createSubscriptionContext as createSubscriptionContext2 } from './src/tree/azure/VSCodeAuthentication';
export * from './src/tree/azure/grouping/GroupingItem';
export * from './src/tree/azure/grouping/LocationGroupingItem';
export * from './src/tree/azure/grouping/ResourceGroupGroupingItem';
export * from './src/tree/azure/grouping/ResourceTypeGroupingItem';
export * from './src/tree/azure/SubscriptionItem';
export * from './src/tree/BranchDataItemWrapper';
export * from './src/tree/InvalidItem';
export { ResourceGroupsItem } from './src/tree/ResourceGroupsItem';
export * from './src/utils/azureClients';
export * from './src/utils/settingUtils';
export * from './src/utils/wrapFunctionsInTelemetry';
// NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen

Просмотреть файл

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { gulp_installAzureAccount, gulp_webpack } from '@microsoft/vscode-azext-dev';
import { gulp_webpack } from '@microsoft/vscode-azext-dev';
import * as fs from 'fs/promises';
import * as gulp from 'gulp';
import * as path from 'path';
@ -50,6 +50,5 @@ async function cleanReadme(): Promise<void> {
exports['webpack-dev'] = gulp.series(prepareForWebpack, () => gulp_webpack('development'));
exports['webpack-prod'] = gulp.series(prepareForWebpack, () => gulp_webpack('production'));
exports.preTest = gulp_installAzureAccount;
exports.listIcons = listIcons;
exports.cleanReadme = cleanReadme;

1024
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -525,7 +525,8 @@
"%azureResourceGroups.deleteConfirmation.EnterName%",
"%azureResourceGroups.deleteConfirmation.ClickButton%"
],
"default": "EnterName"
"default": "EnterName",
"scope": "machine"
},
"azureResourceGroups.showHiddenTypes": {
"type": "boolean",
@ -535,7 +536,8 @@
"azureResourceGroups.groupBy": {
"type": "string",
"description": "%azureResourceGroups.groupBy%",
"default": "resourceType"
"default": "resourceType",
"scope": "machine"
},
"azureResourceGroups.suppressActivityNotifications": {
"type": "boolean",
@ -625,8 +627,8 @@
"lint": "eslint --ext .ts .",
"lint-fix": "eslint --ext .ts . --fix",
"listIcons": "gulp listIcons",
"pretest": "gulp preTest",
"test": "node ./out/test/runTest.js",
"pretest": "webpack",
"webpack": "tsc && gulp webpack-dev",
"webpack-prod": "npm run build && gulp webpack-prod",
"webpack-profile": "webpack --profile --json --mode production > webpack-stats.json && echo Use http://webpack.github.io/analyse to analyze the stats",
@ -635,6 +637,7 @@
},
"devDependencies": {
"@azure/arm-resources-subscriptions": "^2.1.0",
"@azure/identity": "^4.1.0",
"@microsoft/api-extractor": "^7.33.8",
"@microsoft/eslint-config-azuretools": "^0.2.1",
"@microsoft/vscode-azext-dev": "^2.0.0",
@ -668,7 +671,7 @@
"@azure/arm-resources": "^5.2.0",
"@azure/arm-resources-profile-2020-09-01-hybrid": "^2.1.0",
"@azure/ms-rest-js": "^2.7.0",
"@microsoft/vscode-azext-azureauth": "^2.3.0",
"@microsoft/vscode-azext-azureauth": "^2.4.1",
"@microsoft/vscode-azext-azureutils": "^2.0.0",
"@microsoft/vscode-azext-utils": "^2.4.0",
"buffer": "^6.0.3",

Просмотреть файл

@ -28,7 +28,7 @@ import { TagFileSystem } from './commands/tags/TagFileSystem';
import { registerTagDiagnostics } from './commands/tags/registerTagDiagnostics';
import { ext } from './extensionVariables';
import { AzureResourcesApiInternal } from './hostapi.v2.internal';
import { createVSCodeAzureSubscriptionProviderFactory } from './services/VSCodeAzureSubscriptionProvider';
import { getSubscriptionProviderFactory } from './services/getSubscriptionProviderFactory';
import { BranchDataItemCache } from './tree/BranchDataItemCache';
import { HelpTreeItem } from './tree/HelpTreeItem';
import { ResourceGroupsItem } from './tree/ResourceGroupsItem';
@ -69,7 +69,8 @@ export async function activate(context: vscode.ExtensionContext, perfStats: { lo
activateContext.telemetry.properties.isActivationEvent = 'true';
activateContext.telemetry.measurements.mainFileLoad = (perfStats.loadEndTime - perfStats.loadStartTime) / 1000;
ext.subscriptionProviderFactory = createVSCodeAzureSubscriptionProviderFactory();
ext.subscriptionProviderFactory = getSubscriptionProviderFactory(activateContext);
ext.tagFS = new TagFileSystem(ext.appResourceTree);
context.subscriptions.push(vscode.workspace.registerFileSystemProvider(TagFileSystem.scheme, ext.tagFS));

Просмотреть файл

@ -6,7 +6,7 @@
import { AzureSubscriptionProvider } from "@microsoft/vscode-azext-azureauth";
import { AzExtTreeDataProvider, IAzExtLogOutputChannel, IExperimentationServiceAdapter } from "@microsoft/vscode-azext-utils";
import { AzExtResourceType } from "api/src/AzExtResourceType";
import { DiagnosticCollection, Disposable, ExtensionContext, TreeView, UIKind, env } from "vscode";
import { DiagnosticCollection, Disposable, ExtensionContext, TreeView } from "vscode";
import { ActivityLogTreeItem } from "./activityLog/ActivityLogsTreeItem";
import { TagFileSystem } from "./commands/tags/TagFileSystem";
import { AzureResourcesApiInternal } from "./hostapi.v2.internal";
@ -47,10 +47,6 @@ export namespace ext {
export let subscriptionProviderFactory: () => Promise<AzureSubscriptionProvider>;
// When debugging thru VS Code as a web environment, the UIKind is Desktop. However, if you sideload it into the browser, you must
// change this to UIKind.Web and then webpack it again
export const isWeb: boolean = env.uiKind === UIKind.Web;
export namespace v2 {
export let api: AzureResourcesApiInternal;
}

Просмотреть файл

@ -0,0 +1,42 @@
import { AzureDevOpsSubscriptionProviderInitializer, AzureSubscriptionProvider, createAzureDevOpsSubscriptionProviderFactory } from "@microsoft/vscode-azext-azureauth";
import { IActionContext } from "@microsoft/vscode-azext-utils";
import { createVSCodeAzureSubscriptionProviderFactory } from "./VSCodeAzureSubscriptionProvider";
/**
* Returns a factory function that creates a subscription provider, satisfying the `AzureSubscriptionProvider` interface.
*
* If the `useAzureSubscriptionProvider` is set to `true`, an `AzureDevOpsSubscriptionProviderFactory` is returned.
* Otherwise, a `VSCodeSubscriptionProviderFactory` is returned.
*
*/
export function getSubscriptionProviderFactory(activateContext?: IActionContext): () => Promise<AzureSubscriptionProvider> {
// if this for a nightly test, we want to use the test subscription provider
const useAzureFederatedCredentials: boolean = !/^(false|0)?$/i.test(process.env['AzCode_UseAzureFederatedCredentials'] || '')
if (useAzureFederatedCredentials) {
// when running tests, ensure we throw the errors and they aren't silently swallowed
if (activateContext) {
activateContext.errorHandling.rethrow = useAzureFederatedCredentials;
}
const serviceConnectionId: string | undefined = process.env['AzCode_ServiceConnectionID'];
const domain: string | undefined = process.env['AzCode_ServiceConnectionDomain'];
const clientId: string | undefined = process.env['AzCode_ServiceConnectionClientID'];
if (!serviceConnectionId || !domain || !clientId) {
throw new Error(`Using Azure DevOps federated credentials, but federated service connection is not configured\n
process.env.AzCodeServiceConnectionID: ${serviceConnectionId ? "✅" : "❌"}\n
process.env.AzCodeServiceConnectionDomain: ${domain ? "✅" : "❌"}\n
process.env.AzCodeServiceConnectionClientID: ${clientId ? "✅" : "❌"}\n
`);
}
const initializer: AzureDevOpsSubscriptionProviderInitializer = {
serviceConnectionId,
domain,
clientId,
}
return createAzureDevOpsSubscriptionProviderFactory(initializer);
} else {
return createVSCodeAzureSubscriptionProviderFactory();
}
}

Просмотреть файл

@ -97,13 +97,13 @@ class BasicMockResources extends MockResources {
export const createMockSubscriptionWithFunctions = (): BasicMockResources => {
const mockResources = new BasicMockResources();
ext.testing.overrideAzureServiceFactory = createTestAzureResourcesServiceFactory(mockResources);
ext.testing.overrideAzureServiceFactory = createMockAzureResourcesServiceFactory(mockResources);
const mockAzureSubscriptionProvider = new MockAzureSubscriptionProvider(mockResources);
ext.testing.overrideAzureSubscriptionProvider = () => mockAzureSubscriptionProvider;
return mockResources;
}
export function createTestAzureResourcesServiceFactory(mockResources: MockResources): AzureResourcesServiceFactory {
export function createMockAzureResourcesServiceFactory(mockResources: MockResources): AzureResourcesServiceFactory {
return () => {
return {
async listResources(_context, subscription: AzureSubscription): Promise<GenericResource[]> {

Просмотреть файл

@ -5,10 +5,10 @@
import { TestOutputChannel, TestUserInput } from '@microsoft/vscode-azext-dev';
import * as vscode from 'vscode';
import { ext, registerOnActionStartHandler } from '../extension.bundle';
import { ext, registerOnActionStartHandler, settingUtils } from '../extension.bundle';
export let longRunningTestsEnabled: boolean;
export const userSettings: { key: string, value: unknown }[] = [];
// Runs before all tests
suiteSetup(async function (this: Mocha.Context): Promise<void> {
this.timeout(1 * 60 * 1000);
@ -22,6 +22,19 @@ suiteSetup(async function (this: Mocha.Context): Promise<void> {
// Use `TestUserInput` by default so we get an error if an unexpected call to `context.ui` occurs, rather than timing out
context.ui = new TestUserInput(vscode);
});
const groupBySetting = settingUtils.getWorkspaceSetting('groupBy')
userSettings.push({ key: 'groupBy', value: groupBySetting });
longRunningTestsEnabled = !/^(false|0)?$/i.test(process.env.ENABLE_LONG_RUNNING_TESTS || '');
const deleteConfirmationSetting = settingUtils.getWorkspaceSetting('deleteConfirmation');
userSettings.push({ key: 'deleteConfirmation', value: deleteConfirmationSetting });
longRunningTestsEnabled = !/^(false|0)?$/i.test(process.env['AzCode_UseAzureFederatedCredentials'] || '');
});
suiteTeardown(async function (this: Mocha.Context): Promise<void> {
for (const setting of userSettings) {
// reset the settings to their original values
console.debug(`Resetting setting '${setting.key}' to '${setting.value}'`);
await settingUtils.updateGlobalSetting(setting.key, setting.value);
}
});

122
test/nightly/crud.test.ts Normal file
Просмотреть файл

@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { type Location } from '@azure/arm-resources-subscriptions';
import { createTestActionContext, runWithTestActionContext } from '@microsoft/vscode-azext-dev';
import { AzExtParentTreeItem, IActionContext, LocationListStep, SubscriptionItem, createResourceClient, createResourceGroup, deleteResourceGroupV2, ext, randomUtils, settingUtils } from '../../extension.bundle';
import { longRunningTestsEnabled } from "../global.test";
import assert = require("assert");
let rgName: string;
let locations: Location[];
let testSubscription: SubscriptionItem;
suite('Resource CRUD Operations', function (this: Mocha.Suite): void {
this.timeout(7 * 60 * 1000);
suiteSetup(async function (this: Mocha.Context): Promise<void> {
if (!longRunningTestsEnabled) {
this.skip();
}
ext.testing.overrideAzureServiceFactory = undefined;
ext.testing.overrideAzureSubscriptionProvider = undefined;
const subscriptionTreeItems = await ext.appResourceTree.getChildren() as unknown as SubscriptionItem[];
if (subscriptionTreeItems.length > 0) {
const testContext = await createTestActionContext();
testSubscription = subscriptionTreeItems[0] as SubscriptionItem;
const context = { ...testContext, ...testSubscription.subscription };
locations = await LocationListStep.getLocations(context);
}
rgName = randomUtils.getRandomHexString(12);
});
test('Create Resource Group (single)', async () => {
await runWithTestActionContext('createResourceGroup', async context => {
const testInputs: (string | RegExp)[] = [rgName, locations[0].displayName!];
await context.ui.runWithInputs(testInputs, async () => {
await createResourceGroup(context, testSubscription);
});
assert.ok(await resourceGroupExists(context, rgName));
});
});
test('Create Resource Groups (all locations)', async () => {
await Promise.all(locations.map(async l => {
await runWithTestActionContext('createResourceGroup', async context => {
const testInputs: (string | RegExp)[] = [`${rgName}-${l.name}`, l.displayName!];
await context.ui.runWithInputs(testInputs, async () => {
await createResourceGroup(context, testSubscription);
});
assert.ok(await resourceGroupExists(context, `${rgName}-${l.name}`));
});
}));
});
test('Get Resources', async () => {
const subscriptionTreeItems = await ext.appResourceTree.getChildren();
assert.ok(subscriptionTreeItems.length > 0);
for (const subscription of subscriptionTreeItems) {
const groupTreeItems = await ext.appResourceTree.getChildren(subscription as AzExtParentTreeItem);
await Promise.all(groupTreeItems.map(async g => {
const children = await ext.appResourceTree.getChildren(g as AzExtParentTreeItem);
console.log(children);
}));
}
assert.ok(true);
});
test('Delete Resource (EnterName) - Fails when invalid', async () => {
await settingUtils.updateGlobalSetting('deleteConfirmation', 'EnterName');
await runWithTestActionContext('Delete Resource', async context => {
await context.ui.runWithInputs([rgName, 'rgName'], async () => {
try {
await deleteResourceGroupV2(context);
} catch (_) {
console.debug('Expected error: ', _);
// expected to fail here
}
assert.ok(await resourceGroupExists(context, rgName));
});
});
});
test('Delete Resource (EnterName)', async () => {
await settingUtils.updateGlobalSetting('deleteConfirmation', 'EnterName');
await runWithTestActionContext('Delete Resource', async context => {
await context.ui.runWithInputs([rgName, rgName], async () => {
await deleteResourceGroupV2(context);
});
});
});
test('Delete Resource (ClickButton)', async () => {
await settingUtils.updateGlobalSetting('deleteConfirmation', 'ClickButton');
const deleteArray: string[] = Array(locations.length).fill('Delete');
await runWithTestActionContext('Delete Resource', async context => {
await context.ui.runWithInputs([new RegExp(`${rgName}-`), ...deleteArray], async () => {
await deleteResourceGroupV2(context);
});
});
assert.ok(true);
});
});
async function resourceGroupExists(context: IActionContext, rgName: string): Promise<boolean> {
const client = await createResourceClient([context, testSubscription.subscription]);
try {
await client.resourceGroups.get(rgName);
return true;
} catch (_) {
return false;
}
}

Просмотреть файл

@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { longRunningTestsEnabled } from '../global.test';
export const resourceGroupsToDelete: string[] = [];
// Runs before all nightly tests
suiteSetup(async function (this: Mocha.Context): Promise<void> {
if (longRunningTestsEnabled) {
this.timeout(2 * 60 * 1000);
await vscode.commands.executeCommand('azureResourceGroups.logIn');
}
});

Просмотреть файл

@ -3,27 +3,15 @@
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { downloadAndUnzipVSCode, resolveCliArgsFromVSCodeExecutablePath, runTests } from '@vscode/test-electron';
import * as cp from 'child_process';
import { downloadAndUnzipVSCode, runTests } from '@vscode/test-electron';
import * as path from 'path';
async function main(): Promise<void> {
try {
const vscodeExecutablePath = await downloadAndUnzipVSCode();
const [cli, ...args] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath);
cp.spawnSync(
cli,
[
...args,
'--install-extension', 'ms-vscode.azure-account',
],
{
encoding: 'utf-8',
stdio: 'inherit'
});
const repoRoot: string = path.resolve(__dirname, '..', '..');
await runTests({
vscodeExecutablePath,
extensionDevelopmentPath: repoRoot,