Skip to content

Commit 7a0b25b

Browse files
authored
fix(vscode): Updated bundle feed to use index.json instead of index-v2.json (#8807)
* Updated bundle feed to use index.json instead of index-v2.json * Added unit tests to improve coverage by testing feed versions vs local
1 parent 4900760 commit 7a0b25b

File tree

2 files changed

+231
-46
lines changed

2 files changed

+231
-46
lines changed

apps/vs-code-designer/src/app/utils/__test__/bundleFeed.test.ts

Lines changed: 219 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
1-
import { getBundleVersionNumber, getExtensionBundleFolder } from '../bundleFeed';
1+
import {
2+
getBundleVersionNumber,
3+
getExtensionBundleFolder,
4+
getLatestVersionRange,
5+
addDefaultBundle,
6+
downloadExtensionBundle,
7+
} from '../bundleFeed';
28
import { describe, it, expect, vi, beforeEach } from 'vitest';
39
import * as fse from 'fs-extra';
410
import * as path from 'path';
511
import * as cp from 'child_process';
6-
import { extensionBundleId } from '../../../constants';
12+
import { extensionBundleId, defaultVersionRange, defaultExtensionBundlePathValue } from '../../../constants';
13+
import type { IHostJsonV2 } from '@microsoft/vscode-extension-logic-apps';
714
import * as cpUtils from '../funcCoreTools/cpUtils';
15+
import * as feedModule from '../feed';
16+
import * as binariesModule from '../binaries';
17+
18+
// Mock fs-extra
19+
vi.mock('fs-extra', async (importOriginal) => {
20+
const actual = await importOriginal();
21+
return {
22+
...(actual as object),
23+
readdir: vi.fn(),
24+
stat: vi.fn(),
25+
pathExists: vi.fn(),
26+
readdirSync: vi.fn(),
27+
statSync: vi.fn(),
28+
};
29+
});
830

931
// Mock localize
1032
vi.mock('../../localize', () => ({
@@ -47,6 +69,21 @@ vi.mock('vscode', () => ({
4769
},
4870
}));
4971

72+
// Mock feed module
73+
vi.mock('../feed', () => ({
74+
getJsonFeed: vi.fn(),
75+
}));
76+
77+
// Mock binaries module
78+
vi.mock('../binaries', () => ({
79+
downloadAndExtractDependency: vi.fn(),
80+
}));
81+
82+
// Mock localSettings
83+
vi.mock('../appSettings/localSettings', () => ({
84+
getLocalSettingsJson: vi.fn().mockResolvedValue({}),
85+
}));
86+
5087
const mockedFse = vi.mocked(fse);
5188
const mockedExecSync = vi.mocked(cp.execSync);
5289
const mockedExecuteCommand = vi.mocked(cpUtils.executeCommand);
@@ -379,3 +416,183 @@ describe('getBundleVersionNumber', () => {
379416
expect(mockedExecuteCommand).toHaveBeenCalledWith(expect.anything(), '/mock/workspace', 'func', 'GetExtensionBundlePath');
380417
});
381418
});
419+
420+
describe('getLatestVersionRange', () => {
421+
it('should return the default version range constant', () => {
422+
const result = getLatestVersionRange();
423+
expect(result).toBe(defaultVersionRange);
424+
});
425+
426+
it('should return a valid semver range string', () => {
427+
const result = getLatestVersionRange();
428+
expect(result).toMatch(/^\[.*\)$/);
429+
});
430+
});
431+
432+
describe('addDefaultBundle', () => {
433+
it('should add extension bundle configuration to host.json', () => {
434+
const hostJson: IHostJsonV2 = {
435+
version: '2.0',
436+
};
437+
438+
addDefaultBundle(hostJson);
439+
440+
expect(hostJson.extensionBundle).toBeDefined();
441+
expect(hostJson.extensionBundle?.id).toBe(extensionBundleId);
442+
expect(hostJson.extensionBundle?.version).toBe(defaultVersionRange);
443+
});
444+
445+
it('should overwrite existing extension bundle configuration', () => {
446+
const hostJson: IHostJsonV2 = {
447+
version: '2.0',
448+
extensionBundle: {
449+
id: 'old-bundle-id',
450+
version: '[1.0.0, 2.0.0)',
451+
},
452+
};
453+
454+
addDefaultBundle(hostJson);
455+
456+
expect(hostJson.extensionBundle?.id).toBe(extensionBundleId);
457+
expect(hostJson.extensionBundle?.version).toBe(defaultVersionRange);
458+
});
459+
460+
it('should preserve other host.json properties', () => {
461+
const hostJson: IHostJsonV2 = {
462+
version: '2.0',
463+
logging: {
464+
logLevel: {
465+
default: 'Information',
466+
},
467+
},
468+
};
469+
470+
addDefaultBundle(hostJson);
471+
472+
expect(hostJson.version).toBe('2.0');
473+
expect(hostJson.logging).toBeDefined();
474+
expect(hostJson.extensionBundle).toBeDefined();
475+
});
476+
});
477+
478+
describe('downloadExtensionBundle', () => {
479+
const mockedGetJsonFeed = vi.mocked(feedModule.getJsonFeed);
480+
const mockedDownloadAndExtract = vi.mocked(binariesModule.downloadAndExtractDependency);
481+
482+
const createMockContext = () => ({
483+
telemetry: {
484+
properties: {} as Record<string, string>,
485+
measurements: {} as Record<string, number>,
486+
},
487+
});
488+
489+
beforeEach(() => {
490+
vi.clearAllMocks();
491+
// Reset environment variables
492+
delete process.env.AzureFunctionsJobHost_extensionBundle_version;
493+
delete process.env.FUNCTIONS_EXTENSIONBUNDLE_SOURCE_URI;
494+
});
495+
496+
it('should download newer version when feed has higher version than local', async () => {
497+
// Feed versions (simulating index.json format)
498+
const feedVersions = ['1.0.0', '1.1.0', '1.2.0', '1.3.0', '1.95.0'];
499+
500+
// Local version is 1.75.0
501+
mockedFse.pathExists.mockResolvedValue(true as never);
502+
mockedFse.readdirSync.mockReturnValue(['1.75.0'] as any);
503+
mockedFse.statSync.mockReturnValue({ isDirectory: () => true } as any);
504+
505+
// Mock the feed to return the versions array
506+
mockedGetJsonFeed.mockResolvedValue(feedVersions as any);
507+
508+
// Mock download to succeed
509+
mockedDownloadAndExtract.mockResolvedValue(undefined);
510+
511+
const context = createMockContext();
512+
const result = await downloadExtensionBundle(context as any);
513+
514+
// Should have downloaded
515+
expect(result).toBe(true);
516+
expect(context.telemetry.properties.didUpdateExtensionBundle).toBe('true');
517+
518+
// Should download version 1.95.0 (the highest from feed)
519+
expect(mockedDownloadAndExtract).toHaveBeenCalledWith(
520+
expect.anything(),
521+
expect.stringContaining('1.95.0'),
522+
defaultExtensionBundlePathValue,
523+
extensionBundleId,
524+
'1.95.0'
525+
);
526+
});
527+
528+
it('should not download when local version is higher than feed versions', async () => {
529+
// Feed only has older versions
530+
const feedVersions = ['1.0.0', '1.1.0', '1.2.0'];
531+
532+
// Local version is already 1.75.0
533+
mockedFse.pathExists.mockResolvedValue(true as never);
534+
mockedFse.readdirSync.mockReturnValue(['1.75.0'] as any);
535+
mockedFse.statSync.mockReturnValue({ isDirectory: () => true } as any);
536+
537+
mockedGetJsonFeed.mockResolvedValue(feedVersions as any);
538+
539+
const context = createMockContext();
540+
const result = await downloadExtensionBundle(context as any);
541+
542+
// Should not download
543+
expect(result).toBe(false);
544+
expect(context.telemetry.properties.didUpdateExtensionBundle).toBe('false');
545+
expect(mockedDownloadAndExtract).not.toHaveBeenCalled();
546+
});
547+
548+
it('should correctly identify the latest version from an unordered feed list', async () => {
549+
// Feed versions in random order
550+
const feedVersions = ['1.3.0', '1.95.0', '1.0.0', '1.50.0', '1.1.0'];
551+
552+
// No local versions
553+
mockedFse.pathExists.mockResolvedValue(true as never);
554+
mockedFse.readdirSync.mockReturnValue([] as any);
555+
556+
mockedGetJsonFeed.mockResolvedValue(feedVersions as any);
557+
mockedDownloadAndExtract.mockResolvedValue(undefined);
558+
559+
const context = createMockContext();
560+
const result = await downloadExtensionBundle(context as any);
561+
562+
expect(result).toBe(true);
563+
// Should download 1.95.0 (the actual highest version)
564+
expect(mockedDownloadAndExtract).toHaveBeenCalledWith(
565+
expect.anything(),
566+
expect.stringContaining('1.95.0'),
567+
defaultExtensionBundlePathValue,
568+
extensionBundleId,
569+
'1.95.0'
570+
);
571+
});
572+
573+
it('should handle multiple local versions and compare against highest', async () => {
574+
// Feed has 1.95.0
575+
const feedVersions = ['1.0.0', '1.95.0'];
576+
577+
// Multiple local versions, highest is 1.75.0
578+
mockedFse.pathExists.mockResolvedValue(true as never);
579+
mockedFse.readdirSync.mockReturnValue(['1.50.0', '1.75.0', '1.60.0'] as any);
580+
mockedFse.statSync.mockReturnValue({ isDirectory: () => true } as any);
581+
582+
mockedGetJsonFeed.mockResolvedValue(feedVersions as any);
583+
mockedDownloadAndExtract.mockResolvedValue(undefined);
584+
585+
const context = createMockContext();
586+
const result = await downloadExtensionBundle(context as any);
587+
588+
// Should download since 1.95.0 > 1.75.0
589+
expect(result).toBe(true);
590+
expect(mockedDownloadAndExtract).toHaveBeenCalledWith(
591+
expect.anything(),
592+
expect.stringContaining('1.95.0'),
593+
defaultExtensionBundlePathValue,
594+
extensionBundleId,
595+
'1.95.0'
596+
);
597+
});
598+
});

apps/vs-code-designer/src/app/utils/bundleFeed.ts

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { getLocalSettingsJson } from './appSettings/localSettings';
77
import { downloadAndExtractDependency } from './binaries';
88
import { getJsonFeed } from './feed';
99
import type { IActionContext } from '@microsoft/vscode-azext-utils';
10-
import type { IBundleDependencyFeed, IBundleFeed, IBundleMetadata, IHostJsonV2 } from '@microsoft/vscode-extension-logic-apps';
10+
import type { IBundleDependencyFeed, IBundleMetadata, IHostJsonV2 } from '@microsoft/vscode-extension-logic-apps';
1111
import * as path from 'path';
1212
import * as semver from 'semver';
1313
import * as vscode from 'vscode';
@@ -16,38 +16,16 @@ import { ext } from '../../extensionVariables';
1616
import { getFunctionsCommand } from './funcCoreTools/funcVersion';
1717
import * as fse from 'fs-extra';
1818
import { executeCommand } from './funcCoreTools/cpUtils';
19-
/**
20-
* Gets bundle extension feed.
21-
* @param {IActionContext} context - Command context.
22-
* @param {IBundleMetadata | undefined} bundleMetadata - Bundle meta data.
23-
* @returns {Promise<IBundleFeed>} Returns bundle extension object.
24-
*/
25-
async function getBundleFeed(context: IActionContext, bundleMetadata: IBundleMetadata | undefined): Promise<IBundleFeed> {
26-
const bundleId: string = (bundleMetadata && bundleMetadata.id) || extensionBundleId;
27-
28-
const envVarUri: string | undefined = process.env.FUNCTIONS_EXTENSIONBUNDLE_SOURCE_URI;
29-
// Only use an aka.ms link for the most common case, otherwise we will dynamically construct the url
30-
let url: string;
31-
if (!envVarUri && bundleId === extensionBundleId) {
32-
url = 'https://aka.ms/AAqvc78';
33-
} else {
34-
const baseUrl: string = envVarUri || 'https://cdn.functions.azure.com/public';
35-
url = `${baseUrl}/ExtensionBundles/${bundleId}/index-v2.json`;
36-
}
37-
38-
return getJsonFeed(context, url);
39-
}
4019

4120
/**
4221
* Gets Workflow bundle extension feed.
4322
* @param {IActionContext} context - Command context.
44-
* @param {IBundleMetadata | undefined} bundleMetadata - Bundle meta data.
45-
* @returns {Promise<IBundleFeed>} Returns bundle extension object.
23+
* @returns {Promise<string[]>} Returns array of available bundle versions.
4624
*/
47-
async function getWorkflowBundleFeed(context: IActionContext): Promise<IBundleFeed> {
25+
async function getWorkflowBundleFeed(context: IActionContext): Promise<string[]> {
4826
const envVarUri: string | undefined = process.env.FUNCTIONS_EXTENSIONBUNDLE_SOURCE_URI;
4927
const baseUrl: string = envVarUri || 'https://cdn.functions.azure.com/public';
50-
const url = `${baseUrl}/ExtensionBundles/${extensionBundleId}/index-v2.json`;
28+
const url = `${baseUrl}/ExtensionBundles/${extensionBundleId}/index.json`;
5129

5230
return getJsonFeed(context, url);
5331
}
@@ -56,7 +34,7 @@ async function getWorkflowBundleFeed(context: IActionContext): Promise<IBundleFe
5634
* Gets extension bundle dependency feed.
5735
* @param {IActionContext} context - Command context.
5836
* @param {IBundleMetadata | undefined} bundleMetadata - Bundle meta data.
59-
* @returns {Promise<IBundleFeed>} Returns bundle extension object.
37+
* @returns {Promise<IBundleDependencyFeed>} Returns bundle extension object.
6038
*/
6139
async function getBundleDependencyFeed(
6240
context: IActionContext,
@@ -77,12 +55,10 @@ async function getBundleDependencyFeed(
7755

7856
/**
7957
* Gets latest bundle extension version range.
80-
* @param {IActionContext} context - Command context.
81-
* @returns {Promise<string>} Returns lates version range.
58+
* @returns {string} Returns latest version range.
8259
*/
83-
export async function getLatestVersionRange(context: IActionContext): Promise<string> {
84-
const feed: IBundleFeed = await getBundleFeed(context, undefined);
85-
return feed.defaultVersionRange;
60+
export function getLatestVersionRange(): string {
61+
return defaultVersionRange;
8662
}
8763

8864
/**
@@ -97,20 +73,12 @@ export async function getDependenciesVersion(context: IActionContext): Promise<I
9773

9874
/**
9975
* Add bundle extension version to host.json configuration.
100-
* @param {IActionContext} context - Command context.
10176
* @param {IHostJsonV2} hostJson - Host.json configuration.
10277
*/
103-
export async function addDefaultBundle(context: IActionContext, hostJson: IHostJsonV2): Promise<void> {
104-
let versionRange: string;
105-
try {
106-
versionRange = await getLatestVersionRange(context);
107-
} catch {
108-
versionRange = defaultVersionRange;
109-
}
110-
78+
export function addDefaultBundle(hostJson: IHostJsonV2): void {
11179
hostJson.extensionBundle = {
11280
id: extensionBundleId,
113-
version: versionRange,
81+
version: defaultVersionRange,
11482
};
11583
}
11684

@@ -191,8 +159,8 @@ export async function downloadExtensionBundle(context: IActionContext): Promise<
191159

192160
// Check the latest from feed.
193161
let latestFeedBundleVersion = '1.0.0';
194-
const feed: IBundleFeed = await getWorkflowBundleFeed(context);
195-
for (const bundleVersion in feed.bundleVersions) {
162+
const feedVersions: string[] = await getWorkflowBundleFeed(context);
163+
for (const bundleVersion of feedVersions) {
196164
latestFeedBundleVersion = semver.gt(latestFeedBundleVersion, bundleVersion) ? latestFeedBundleVersion : bundleVersion;
197165
}
198166

0 commit comments

Comments
 (0)