Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions Localize/lang/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
"19gdw8": "Learn more",
"1A1P5b": "Comment",
"1AFYij": "This template contains one workflow, therefore it is classified as a Workflow.",
"1Ch1Od": "This name is reserved and cannot be used as a workflow name.",
"1D047X": "Required. The value to convert to XML.",
"1Fn5n+": "Required. The URI encoded string.",
"1GWzEL": "No results found for the specified filters",
Expand Down Expand Up @@ -2123,6 +2124,7 @@
"_19gdw8.comment": "Label to learn more about creating a new connection",
"_1A1P5b.comment": "Comment Label",
"_1AFYij.comment": "Content for the toaster for adding a single workflow.",
"_1Ch1Od.comment": "Error message when workflow name matches a reserved project folder name",
"_1D047X.comment": "Required string parameter to be converted using xml function",
"_1Fn5n+.comment": "Required URI encoded string parameter to be converted using uriComponentToBinary function",
"_1GWzEL.comment": "Text displayed when no results are found in the browse grid",
Expand Down Expand Up @@ -3940,6 +3942,7 @@
"_aGxYMY.comment": "Label to clear editor",
"_aGyVJT.comment": "Required number parameter to get number of objects to remove for skip function",
"_aI9W5L.comment": "Accessibility text to inform user this template does not contain connectors",
"_aIk72V.comment": "Warning message when workflow name collides with an existing workflow in the project",
"_aJg1gl.comment": "error message for maximum invalid retry interval",
"_aKf/r+.comment": "Run identifier not found error message",
"_aP1wk9.comment": "Label for the description of a custom 'indexOf' function",
Expand Down Expand Up @@ -5319,6 +5322,7 @@
"aGxYMY": "Clear editor",
"aGyVJT": "Required. The number of objects to remove from the front of Collection. Must be a positive integer.",
"aI9W5L": "This template does not have connectors",
"aIk72V": "A workflow with this name already exists in the selected project.",
"aJg1gl": "Retry policy maximum interval is invalid, must match ISO 8601 duration format",
"aKf/r+": "Specified run identifier not found",
"aP1wk9": "Returns the first index of a value within a string (case-insensitive, invariant culture)",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { validateFuncCoreToolsIsLatest } from '../validateFuncCoreToolsIsLatest';
import { useBinariesDependencies, binariesExist } from '../../../utils/binaries';
import {
useBinariesDependencies,
binariesExist,
verifyDependencyIntegrity,
getLatestFunctionCoreToolsVersion,
} from '../../../utils/binaries';
import { isDevContainerWorkspace } from '../../../utils/devContainerUtils';
import { getLocalFuncCoreToolsVersion } from '../../../utils/funcCoreTools/funcVersion';
import { installFuncCoreToolsBinaries } from '../installFuncCoreTools';

// Factory mocks required to break problematic import chains:
// - binaries: circular dep with onboarding.ts
Expand All @@ -10,6 +17,7 @@ vi.mock('../../../utils/binaries', () => ({
useBinariesDependencies: vi.fn(),
binariesExist: vi.fn(),
getLatestFunctionCoreToolsVersion: vi.fn(),
verifyDependencyIntegrity: vi.fn(),
installBinaries: vi.fn(),
getCpuArchitecture: vi.fn(),
}));
Expand Down Expand Up @@ -106,4 +114,33 @@ describe('validateFuncCoreToolsIsLatest', () => {
expect(binariesExist).not.toHaveBeenCalled();
});
});

describe('on-disk integrity behavior', () => {
it('reinstalls when binaries exist but the on-disk integrity check fails', async () => {
vi.mocked(isDevContainerWorkspace).mockResolvedValue(false);
vi.mocked(useBinariesDependencies).mockResolvedValue(true);
vi.mocked(binariesExist).mockResolvedValue(true);
vi.mocked(verifyDependencyIntegrity).mockReturnValue(false);
vi.mocked(getLocalFuncCoreToolsVersion).mockResolvedValue('4.0.0');
vi.mocked(getLatestFunctionCoreToolsVersion).mockResolvedValue('4.0.0');

await validateFuncCoreToolsIsLatest('4');

expect(verifyDependencyIntegrity).toHaveBeenCalledWith(expect.anything(), 'FuncCoreTools');
expect(installFuncCoreToolsBinaries).toHaveBeenCalled();
});

it('does not reinstall when binaries exist, integrity passes, and the version is current', async () => {
vi.mocked(isDevContainerWorkspace).mockResolvedValue(false);
vi.mocked(useBinariesDependencies).mockResolvedValue(true);
vi.mocked(binariesExist).mockResolvedValue(true);
vi.mocked(verifyDependencyIntegrity).mockReturnValue(true);
vi.mocked(getLocalFuncCoreToolsVersion).mockResolvedValue('4.0.0');
vi.mocked(getLatestFunctionCoreToolsVersion).mockResolvedValue('4.0.0');

await validateFuncCoreToolsIsLatest('4');

expect(installFuncCoreToolsBinaries).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { PackageManager, funcDependencyName } from '../../../constants';
import { localize } from '../../../localize';
import { executeOnFunctions } from '../../functionsExtension/executeOnFunctionsExt';
import { binariesExist, getLatestFunctionCoreToolsVersion, useBinariesDependencies } from '../../utils/binaries';
import { binariesExist, getLatestFunctionCoreToolsVersion, useBinariesDependencies, verifyDependencyIntegrity } from '../../utils/binaries';
import { startAllDesignTimeApis, stopAllDesignTimeApis } from '../../utils/codeless/startDesignTimeApi';
import { getFunctionsCommand, getLocalFuncCoreToolsVersion, tryParseFuncVersion } from '../../utils/funcCoreTools/funcVersion';
import { getBrewPackageName } from '../../utils/funcCoreTools/getBrewPackageName';
Expand Down Expand Up @@ -38,14 +38,18 @@ async function validateFuncCoreToolsIsLatestBinaries(majorVersion?: string): Pro

const binaries = await binariesExist(funcDependencyName);
context.telemetry.properties.binariesExist = `${binaries}`;
// Deep-verify the installed files against the on-disk integrity manifest so a corrupt/incomplete
// install (e.g. a removed Function Host DLL) forces a wipe + reinstall instead of failing at startup.
const integrityValid = binaries ? await verifyDependencyIntegrity(context, funcDependencyName) : false;
context.telemetry.properties.integrityValid = `${integrityValid}`;

const localVersion: string | null = binaries ? await getLocalFuncCoreToolsVersion() : null;
context.telemetry.properties.localVersion = localVersion ?? 'null';

const newestVersion: string | undefined = binaries ? await getLatestFunctionCoreToolsVersion(context, majorVersion) : undefined;
const isOutdated = binaries && localVersion && newestVersion && semver.gt(newestVersion, localVersion);

const shouldInstall = !binaries || localVersion === null || isOutdated;
const shouldInstall = !binaries || !integrityValid || localVersion === null || isOutdated;

if (shouldInstall) {
if (isOutdated) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as vscode from 'vscode';
import { nodeJsDependencyName } from '../../../../constants';
import { ext } from '../../../../extensionVariables';
import { binariesExist, getLatestNodeJsVersion } from '../../../utils/binaries';
import { binariesExist, getLatestNodeJsVersion, verifyDependencyIntegrity } from '../../../utils/binaries';
import { getLocalNodeJsVersion, getNodeJsCommand, setNodeJsCommand } from '../../../utils/nodeJs/nodeJsVersion';
import { getWorkspaceSetting, updateGlobalSetting } from '../../../utils/vsCodeConfig/settings';
import { installNodeJs } from '../installNodeJs';
Expand Down Expand Up @@ -38,6 +38,7 @@ vi.mock('../../../../extensionVariables', () => ({
vi.mock('../../../utils/binaries', () => ({
binariesExist: vi.fn(),
getLatestNodeJsVersion: vi.fn(),
verifyDependencyIntegrity: vi.fn(() => true),
}));

vi.mock('../../../utils/nodeJs/nodeJsVersion', () => ({
Expand Down Expand Up @@ -81,6 +82,31 @@ describe('validateNodeJsIsLatest', () => {
vi.mocked(vscode.window.showErrorMessage).mockResolvedValue(undefined);
});

it('reinstalls when binaries exist but the on-disk integrity check fails', async () => {
vi.mocked(binariesExist).mockResolvedValue(true);
vi.mocked(verifyDependencyIntegrity).mockReturnValue(false);

await validateNodeJsIsLatest('20');

expect(verifyDependencyIntegrity).toHaveBeenCalledWith(contextRef.current, nodeJsDependencyName);
expect(installNodeJs).toHaveBeenCalledWith(contextRef.current, '20');
expect(contextRef.current.telemetry.properties.integrityValid).toBe('false');
expect(getLocalNodeJsVersion).not.toHaveBeenCalled();
});

it('does not reinstall when binaries exist and the on-disk integrity check passes', async () => {
vi.mocked(getWorkspaceSetting).mockReturnValue(true);
vi.mocked(binariesExist).mockResolvedValue(true);
vi.mocked(verifyDependencyIntegrity).mockReturnValue(true);
vi.mocked(getLocalNodeJsVersion).mockResolvedValue('18.0.0');
vi.mocked(getLatestNodeJsVersion).mockResolvedValue('18.0.0');

await validateNodeJsIsLatest('18');

expect(installNodeJs).not.toHaveBeenCalled();
expect(contextRef.current.telemetry.properties.integrityValid).toBe('true');
});

it('installs without checking GitHub latest version when binaries are missing', async () => {
vi.mocked(binariesExist).mockResolvedValue(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { nodeJsDependencyName } from '../../../constants';
import { ext } from '../../../extensionVariables';
import { localize } from '../../../localize';
import { binariesExist, getLatestNodeJsVersion } from '../../utils/binaries';
import { binariesExist, getLatestNodeJsVersion, verifyDependencyIntegrity } from '../../utils/binaries';
import { getLocalNodeJsVersion, getNodeJsCommand, setNodeJsCommand } from '../../utils/nodeJs/nodeJsVersion';
import { getWorkspaceSetting, updateGlobalSetting } from '../../utils/vsCodeConfig/settings';
import { installNodeJs } from './installNodeJs';
Expand All @@ -23,8 +23,12 @@ export async function validateNodeJsIsLatest(majorVersion?: string): Promise<voi
await setNodeJsCommand();
const binaries = await binariesExist(nodeJsDependencyName);
context.telemetry.properties.binariesExist = `${binaries}`;
// Deep-verify the installed files against the on-disk integrity manifest so a corrupt/incomplete
// install (e.g. a removed file) forces a wipe + reinstall instead of silently failing at runtime.
const integrityValid = binaries ? await verifyDependencyIntegrity(context, nodeJsDependencyName) : false;
context.telemetry.properties.integrityValid = `${integrityValid}`;

if (!binaries) {
if (!binaries || !integrityValid) {
await installNodeJs(context, majorVersion);
context.telemetry.properties.binaryCommand = `${getNodeJsCommand()}`;
} else if (showNodeJsWarning) {
Expand Down
Loading
Loading