|
1 | | -import { getBundleVersionNumber, getExtensionBundleFolder } from '../bundleFeed'; |
| 1 | +import { |
| 2 | + getBundleVersionNumber, |
| 3 | + getExtensionBundleFolder, |
| 4 | + getLatestVersionRange, |
| 5 | + addDefaultBundle, |
| 6 | + downloadExtensionBundle, |
| 7 | +} from '../bundleFeed'; |
2 | 8 | import { describe, it, expect, vi, beforeEach } from 'vitest'; |
3 | 9 | import * as fse from 'fs-extra'; |
4 | 10 | import * as path from 'path'; |
5 | 11 | 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'; |
7 | 14 | 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 | +}); |
8 | 30 |
|
9 | 31 | // Mock localize |
10 | 32 | vi.mock('../../localize', () => ({ |
@@ -47,6 +69,21 @@ vi.mock('vscode', () => ({ |
47 | 69 | }, |
48 | 70 | })); |
49 | 71 |
|
| 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 | + |
50 | 87 | const mockedFse = vi.mocked(fse); |
51 | 88 | const mockedExecSync = vi.mocked(cp.execSync); |
52 | 89 | const mockedExecuteCommand = vi.mocked(cpUtils.executeCommand); |
@@ -379,3 +416,183 @@ describe('getBundleVersionNumber', () => { |
379 | 416 | expect(mockedExecuteCommand).toHaveBeenCalledWith(expect.anything(), '/mock/workspace', 'func', 'GetExtensionBundlePath'); |
380 | 417 | }); |
381 | 418 | }); |
| 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 | +}); |
0 commit comments