Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions apps/vs-code-designer/src/__test__/nodeUtilCompatibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import util from 'node:util';

type LegacyNodeUtil = {
isArray?: (value: unknown) => value is unknown[];
isNullOrUndefined?: (value: unknown) => value is null | undefined;
isNumber?: (value: unknown) => value is number;
};

const legacyUtil = util as LegacyNodeUtil;
const originalHelpers = {
isArray: legacyUtil.isArray,
isNullOrUndefined: legacyUtil.isNullOrUndefined,
isNumber: legacyUtil.isNumber,
};

function restoreHelper<T extends keyof LegacyNodeUtil>(name: T, value: LegacyNodeUtil[T]): void {
if (value === undefined) {
delete legacyUtil[name];
} else {
legacyUtil[name] = value;
}
}

describe('applyNodeUtilCompatibility', () => {
afterEach(() => {
restoreHelper('isArray', originalHelpers.isArray);
restoreHelper('isNullOrUndefined', originalHelpers.isNullOrUndefined);
restoreHelper('isNumber', originalHelpers.isNumber);
vi.resetModules();
});

it('restores legacy util helpers removed from newer Node runtimes', async () => {
legacyUtil.isArray = undefined;
legacyUtil.isNullOrUndefined = undefined;
legacyUtil.isNumber = undefined;

const { applyNodeUtilCompatibility } = await import('../nodeUtilCompatibility');
applyNodeUtilCompatibility();

expect(legacyUtil.isArray?.([])).toBe(true);
expect(legacyUtil.isArray?.('value')).toBe(false);
expect(legacyUtil.isNullOrUndefined?.(null)).toBe(true);
expect(legacyUtil.isNullOrUndefined?.(undefined)).toBe(true);
expect(legacyUtil.isNullOrUndefined?.('value')).toBe(false);
expect(legacyUtil.isNumber?.(1)).toBe(true);
expect(legacyUtil.isNumber?.('1')).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { PackageManager, dotnetDependencyName, funcDependencyName, nodeJsDependencyName } from '../../../constants';
import { downloadAndExtractDependency } from '../../utils/binaries';
import { executeCommand } from '../../utils/funcCoreTools/cpUtils';
import { getNpmDistTag } from '../../utils/funcCoreTools/getNpmDistTag';
import { ensureRuntimeDependenciesPath } from '../../utils/runtimeDependenciesPath';
import { promptForFuncVersion } from '../../utils/vsCodeConfig/settings';
import { installDotNet } from '../dotnet/installDotNet';
import { installFuncCoreToolsBinaries, installFuncCoreToolsSystem } from '../funcCoreTools/installFuncCoreTools';
import { installNodeJs } from '../nodeJs/installNodeJs';

vi.mock('../../../extensionVariables', () => ({
ext: {
outputChannel: {
show: vi.fn(),
appendLog: vi.fn(),
},
},
}));

vi.mock('../../utils/runtimeDependenciesPath', () => ({
ensureRuntimeDependenciesPath: vi.fn(),
}));

vi.mock('../../utils/binaries', () => ({
downloadAndExtractDependency: vi.fn(),
getCpuArchitecture: vi.fn(() => 'x64'),
getDotNetBinariesReleaseUrl: vi.fn(() => 'https://dot.net/v1/dotnet-install.ps1'),
getFunctionCoreToolsBinariesReleaseUrl: vi.fn(() => 'https://example.com/func.zip'),
getLatestFunctionCoreToolsVersion: vi.fn(async () => '4.0.0'),
getLatestNodeJsVersion: vi.fn(async () => '20.0.0'),
getNodeJsBinariesReleaseUrl: vi.fn(() => 'https://example.com/node.zip'),
}));

vi.mock('../../utils/funcCoreTools/cpUtils', () => ({
executeCommand: vi.fn(),
}));

vi.mock('../../utils/funcCoreTools/getBrewPackageName', () => ({
getBrewPackageName: vi.fn(() => 'azure-functions-core-tools@4'),
}));

vi.mock('../../utils/funcCoreTools/getNpmDistTag', () => ({
getNpmDistTag: vi.fn(),
}));

vi.mock('../../utils/vsCodeConfig/settings', () => ({
promptForFuncVersion: vi.fn(),
}));

vi.mock('vscode-nls', () => ({
localize: (_key: string, message: string, ...args: unknown[]) =>
message.replace(/{(\d+)}/g, (_match, index) => String(args[Number(index)] ?? '')),
}));

describe('runtime dependency installers', () => {
const context = { telemetry: { properties: {} } } as any;
const originalPlatform = process.platform;

beforeEach(() => {
vi.clearAllMocks();
Object.defineProperty(process, 'platform', { value: originalPlatform });
vi.mocked(ensureRuntimeDependenciesPath).mockResolvedValue('D:\\dependencies');
vi.mocked(downloadAndExtractDependency).mockResolvedValue(undefined);
vi.mocked(getNpmDistTag).mockResolvedValue({ tag: '4.0.0' } as any);
vi.mocked(promptForFuncVersion).mockResolvedValue('4' as any);
});

it('installs .NET into the ensured runtime dependency path', async () => {
await installDotNet(context, '8');

expect(ensureRuntimeDependenciesPath).toHaveBeenCalled();
expect(downloadAndExtractDependency).toHaveBeenCalledWith(
context,
'https://dot.net/v1/dotnet-install.ps1',
'D:\\dependencies',
dotnetDependencyName,
null,
'8'
);
});

it('installs Functions Core Tools binaries into the ensured runtime dependency path', async () => {
await installFuncCoreToolsBinaries(context, '4');

expect(ensureRuntimeDependenciesPath).toHaveBeenCalled();
expect(downloadAndExtractDependency).toHaveBeenCalledWith(
context,
'https://example.com/func.zip',
'D:\\dependencies',
funcDependencyName
);
});

it('uses the macOS Functions Core Tools binary release when running on macOS', async () => {
Object.defineProperty(process, 'platform', { value: 'darwin' });

await installFuncCoreToolsBinaries(context, '4');

expect(downloadAndExtractDependency).toHaveBeenCalledWith(
context,
'https://example.com/func.zip',
'D:\\dependencies',
funcDependencyName
);
});

it('installs Node.js into the ensured runtime dependency path', async () => {
await installNodeJs(context, '20');

expect(ensureRuntimeDependenciesPath).toHaveBeenCalled();
expect(downloadAndExtractDependency).toHaveBeenCalledWith(
context,
'https://example.com/node.zip',
'D:\\dependencies',
nodeJsDependencyName
);
});

it('uses the Linux Node.js binary release when running on Linux', async () => {
Object.defineProperty(process, 'platform', { value: 'linux' });

await installNodeJs(context, '20');

expect(downloadAndExtractDependency).toHaveBeenCalledWith(
context,
'https://example.com/node.zip',
'D:\\dependencies',
nodeJsDependencyName
);
});

it('installs Functions Core Tools through npm when system installation is selected', async () => {
await installFuncCoreToolsSystem(context, [PackageManager.npm], '4' as any);

expect(executeCommand).toHaveBeenCalledWith(expect.anything(), undefined, 'npm', 'install', '-g', 'azure-functions-core-tools@4.0.0');
});

it('installs Functions Core Tools through brew when that package manager is selected', async () => {
await installFuncCoreToolsSystem(context, [PackageManager.brew], '4' as any);

expect(executeCommand).toHaveBeenCalledWith(expect.anything(), undefined, 'brew', 'tap', 'azure/functions');
expect(executeCommand).toHaveBeenCalledWith(expect.anything(), undefined, 'brew', 'install', 'azure-functions-core-tools@4');
});

it('rejects unsupported system package managers', async () => {
await expect(installFuncCoreToolsSystem(context, ['unknown' as PackageManager], '4' as any)).rejects.toThrow('Invalid package manager');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { autoRuntimeDependenciesPathSettingKey, defaultDependencyPathValue } from '../../../constants';
import { ext } from '../../../extensionVariables';
import { localize } from '../../../localize';
import { getDependencyTimeout } from '../../utils/binaries';
Expand All @@ -12,9 +11,9 @@ import { executeCommand } from '../../utils/funcCoreTools/cpUtils';
import { setFunctionsCommand } from '../../utils/funcCoreTools/funcVersion';
import { installLSPSDK } from '../../utils/languageServerProtocol';
import { setNodeJsCommand } from '../../utils/nodeJs/nodeJsVersion';
import { ensureRuntimeDependenciesPath } from '../../utils/runtimeDependenciesPath';
import { runWithDurationTelemetry } from '../../utils/telemetry';
import { timeout } from '../../utils/timeout';
import { getGlobalSetting, updateGlobalSetting } from '../../utils/vsCodeConfig/settings';
import { validateDotNetIsLatest } from '../dotnet/validateDotNetIsLatest';
import { validateFuncCoreToolsIsLatest } from '../funcCoreTools/validateFuncCoreToolsIsLatest';
import { validateNodeJsIsLatest } from '../nodeJs/validateNodeJsIsLatest';
Expand Down Expand Up @@ -43,10 +42,7 @@ export async function validateAndInstallBinaries(context: IActionContext) {
const dependencyTimeout = getDependencyTimeout() * 1000;

context.telemetry.properties.dependencyTimeout = `${dependencyTimeout} milliseconds`;
if (!getGlobalSetting<string>(autoRuntimeDependenciesPathSettingKey)) {
await updateGlobalSetting(autoRuntimeDependenciesPathSettingKey, defaultDependencyPathValue);
context.telemetry.properties.dependencyPath = defaultDependencyPathValue;
}
context.telemetry.properties.dependencyPath = await ensureRuntimeDependenciesPath();

context.telemetry.properties.lastStep = 'getDependenciesVersion';
progress.report({ increment: 10, message: 'Get dependency version from CDN' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { autoRuntimeDependenciesPathSettingKey, dotnetDependencyName } from '../../../constants';
import { dotnetDependencyName } from '../../../constants';
import { ext } from '../../../extensionVariables';
import { downloadAndExtractDependency, getDotNetBinariesReleaseUrl } from '../../utils/binaries';
import { getGlobalSetting } from '../../utils/vsCodeConfig/settings';
import { ensureRuntimeDependenciesPath } from '../../utils/runtimeDependenciesPath';
import type { IActionContext } from '@microsoft/vscode-azext-utils';

export async function installDotNet(context: IActionContext, majorVersion?: string): Promise<void> {
ext.outputChannel.show();
context.telemetry.properties.majorVersion = majorVersion;
const targetDirectory = getGlobalSetting<string>(autoRuntimeDependenciesPathSettingKey);
const targetDirectory = await ensureRuntimeDependenciesPath();

context.telemetry.properties.lastStep = 'getDotNetBinariesReleaseUrl';
const scriptUrl = getDotNetBinariesReleaseUrl();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { PackageManager, autoRuntimeDependenciesPathSettingKey, funcDependencyName, funcPackageName } from '../../../constants';
import { PackageManager, funcDependencyName, funcPackageName } from '../../../constants';
import { ext } from '../../../extensionVariables';
import {
downloadAndExtractDependency,
Expand All @@ -13,15 +13,16 @@ import {
import { executeCommand } from '../../utils/funcCoreTools/cpUtils';
import { getBrewPackageName } from '../../utils/funcCoreTools/getBrewPackageName';
import { getNpmDistTag } from '../../utils/funcCoreTools/getNpmDistTag';
import { getGlobalSetting, promptForFuncVersion } from '../../utils/vsCodeConfig/settings';
import { ensureRuntimeDependenciesPath } from '../../utils/runtimeDependenciesPath';
import { promptForFuncVersion } from '../../utils/vsCodeConfig/settings';
import type { IActionContext } from '@microsoft/vscode-azext-utils';
import { Platform, type FuncVersion, type INpmDistTag } from '@microsoft/vscode-extension-logic-apps';
import { localize } from 'vscode-nls';

export async function installFuncCoreToolsBinaries(context: IActionContext, majorVersion?: string): Promise<void> {
ext.outputChannel.show();
const arch = getCpuArchitecture();
const targetDirectory = getGlobalSetting<string>(autoRuntimeDependenciesPathSettingKey);
const targetDirectory = await ensureRuntimeDependenciesPath();
context.telemetry.properties.lastStep = 'getLatestFunctionCoreToolsVersion';
const version = await getLatestFunctionCoreToolsVersion(context, majorVersion);
let azureFunctionCoreToolsReleasesUrl: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Platform } from '@microsoft/vscode-extension-logic-apps';
import { autoRuntimeDependenciesPathSettingKey, nodeJsDependencyName } from '../../../constants';
import { nodeJsDependencyName } from '../../../constants';
import { ext } from '../../../extensionVariables';
import {
downloadAndExtractDependency,
getCpuArchitecture,
getLatestNodeJsVersion,
getNodeJsBinariesReleaseUrl,
} from '../../utils/binaries';
import { getGlobalSetting } from '../../utils/vsCodeConfig/settings';
import { ensureRuntimeDependenciesPath } from '../../utils/runtimeDependenciesPath';
import type { IActionContext } from '@microsoft/vscode-azext-utils';

export async function installNodeJs(context: IActionContext, majorVersion?: string): Promise<void> {
ext.outputChannel.show();
const arch = getCpuArchitecture();
const targetDirectory = getGlobalSetting<string>(autoRuntimeDependenciesPathSettingKey);
const targetDirectory = await ensureRuntimeDependenciesPath();
context.telemetry.properties.lastStep = 'getLatestNodeJsVersion';
const version = await getLatestNodeJsVersion(context, majorVersion);
let nodeJsReleaseUrl: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('../../../../localize', () => ({
localize: (_key: string, defaultMessage: string) => defaultMessage,
}));

vi.mock('@microsoft/vscode-azext-utils', () => ({
AzureWizardPromptStep: class AzureWizardPromptStep {},
parseError: (error: any) => ({
isUserCancelledError: error?.isUserCancelledError === true,
message: error?.message ?? String(error),
}),
}));

import { AuthenticationMethodSelectionStep } from '../authenticationMethodStep';

describe('AuthenticationMethodSelectionStep', () => {
let context: any;

beforeEach(() => {
context = {
enabled: true,
telemetry: { properties: {}, measurements: {} },
ui: {
showQuickPick: vi.fn(),
},
};
});

it('defaults to connection keys when the authentication prompt is cancelled', async () => {
context.ui.showQuickPick.mockRejectedValue({ isUserCancelledError: true });

await new AuthenticationMethodSelectionStep().prompt(context);

expect(context.authenticationMethod).toBe('rawKeys');
expect(context.telemetry.properties.authenticationMethodDefaulted).toBe('rawKeys');
});

it('keeps explicit Managed Service Identity selections', async () => {
context.ui.showQuickPick.mockResolvedValue({ data: 'managedServiceIdentity' });

await new AuthenticationMethodSelectionStep().prompt(context);

expect(context.authenticationMethod).toBe('managedServiceIdentity');
});

it('keeps explicit connection-key selections', async () => {
context.ui.showQuickPick.mockResolvedValue({ data: 'rawKeys' });

await new AuthenticationMethodSelectionStep().prompt(context);

expect(context.authenticationMethod).toBe('rawKeys');
});

it('does not prompt when Azure connectors are disabled', () => {
context.enabled = false;

expect(new AuthenticationMethodSelectionStep().shouldPrompt(context)).toBe(false);
});

it('rethrows non-cancellation errors', async () => {
context.ui.showQuickPick.mockRejectedValue(new Error('quick pick failed'));

await expect(new AuthenticationMethodSelectionStep().prompt(context)).rejects.toThrow('quick pick failed');
});
});
Loading
Loading