| import type { Logger } from '@n8n/backend-common'; |
| import type { GlobalConfig } from '@n8n/config'; |
| import { LICENSE_FEATURES } from '@n8n/constants'; |
| import { InstalledNodes } from '@n8n/db'; |
| import { InstalledPackages } from '@n8n/db'; |
| import { InstalledNodesRepository } from '@n8n/db'; |
| import { InstalledPackagesRepository } from '@n8n/db'; |
| import axios from 'axios'; |
| import { exec } from 'child_process'; |
| import { mkdir, readFile, writeFile, rm, access, constants } from 'fs/promises'; |
| import { mocked } from 'jest-mock'; |
| import { mock } from 'jest-mock-extended'; |
| import type { InstanceSettings, PackageDirectoryLoader } from 'n8n-core'; |
| import type { PublicInstalledPackage } from 'n8n-workflow'; |
| import { join } from 'node:path'; |
|
|
| import { |
| NODE_PACKAGE_PREFIX, |
| NPM_COMMAND_TOKENS, |
| NPM_PACKAGE_STATUS_GOOD, |
| RESPONSE_ERROR_MESSAGES, |
| } from '@/constants'; |
| import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; |
| import type { CommunityPackages } from '@/interfaces'; |
| import type { License } from '@/license'; |
| import type { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; |
| import type { Publisher } from '@/scaling/pubsub/publisher.service'; |
| import { CommunityPackagesService } from '@/services/community-packages.service'; |
| import { mockInstance } from '@test/mocking'; |
| import { COMMUNITY_NODE_VERSION, COMMUNITY_PACKAGE_VERSION } from '@test-integration/constants'; |
| import { randomName } from '@test-integration/random'; |
| import { mockPackageName, mockPackagePair } from '@test-integration/utils'; |
|
|
| jest.mock('fs/promises'); |
| jest.mock('child_process'); |
| jest.mock('axios'); |
|
|
| type ExecOptions = NonNullable<Parameters<typeof exec>[1]>; |
| type ExecCallback = NonNullable<Parameters<typeof exec>[2]>; |
|
|
| const execMock = ((...args) => { |
| const cb = args[args.length - 1] as ExecCallback; |
| cb(null, 'Done', ''); |
| }) as typeof exec; |
|
|
| describe('CommunityPackagesService', () => { |
| const license = mock<License>(); |
| const globalConfig = mock<GlobalConfig>({ |
| nodes: { |
| communityPackages: { |
| reinstallMissing: false, |
| registry: 'some.random.host', |
| unverifiedEnabled: true, |
| }, |
| }, |
| }); |
| const loadNodesAndCredentials = mock<LoadNodesAndCredentials>(); |
| const installedNodesRepository = mockInstance(InstalledNodesRepository); |
| const installedPackageRepository = mockInstance(InstalledPackagesRepository); |
|
|
| const nodesDownloadDir = '/tmp/n8n-jest-global-downloads'; |
| const instanceSettings = mock<InstanceSettings>({ nodesDownloadDir }); |
|
|
| const logger = mock<Logger>(); |
| const publisher = mock<Publisher>(); |
|
|
| const communityPackagesService = new CommunityPackagesService( |
| instanceSettings, |
| logger, |
| installedPackageRepository, |
| loadNodesAndCredentials, |
| publisher, |
| license, |
| globalConfig, |
| ); |
|
|
| beforeEach(() => { |
| jest.resetAllMocks(); |
| loadNodesAndCredentials.postProcessLoaders.mockResolvedValue(undefined); |
|
|
| const nodeName = randomName(); |
| installedNodesRepository.create.mockImplementation(() => { |
| return Object.assign(new InstalledNodes(), { |
| name: nodeName, |
| type: nodeName, |
| latestVersion: COMMUNITY_NODE_VERSION.CURRENT, |
| }); |
| }); |
| installedPackageRepository.create.mockImplementation(() => { |
| return Object.assign(new InstalledPackages(), { |
| packageName: mockPackageName(), |
| installedVersion: COMMUNITY_PACKAGE_VERSION.CURRENT, |
| }); |
| }); |
| }); |
|
|
| describe('parseNpmPackageName()', () => { |
| test('should fail with empty package name', () => { |
| expect(() => communityPackagesService.parseNpmPackageName('')).toThrowError(); |
| }); |
|
|
| test('should fail with invalid package prefix name', () => { |
| expect(() => |
| communityPackagesService.parseNpmPackageName('INVALID_PREFIX@123'), |
| ).toThrowError(); |
| }); |
|
|
| test('should parse valid package name', () => { |
| const name = mockPackageName(); |
| const parsed = communityPackagesService.parseNpmPackageName(name); |
|
|
| expect(parsed.rawString).toBe(name); |
| expect(parsed.packageName).toBe(name); |
| expect(parsed.scope).toBeUndefined(); |
| expect(parsed.version).toBeUndefined(); |
| }); |
|
|
| test('should parse valid package name and version', () => { |
| const name = mockPackageName(); |
| const version = '0.1.1'; |
| const fullPackageName = `${name}@${version}`; |
| const parsed = communityPackagesService.parseNpmPackageName(fullPackageName); |
|
|
| expect(parsed.rawString).toBe(fullPackageName); |
| expect(parsed.packageName).toBe(name); |
| expect(parsed.scope).toBeUndefined(); |
| expect(parsed.version).toBe(version); |
| }); |
|
|
| test('should parse valid package name, scope and version', () => { |
| const scope = '@n8n'; |
| const name = mockPackageName(); |
| const version = '0.1.1'; |
| const fullPackageName = `${scope}/${name}@${version}`; |
| const parsed = communityPackagesService.parseNpmPackageName(fullPackageName); |
|
|
| expect(parsed.rawString).toBe(fullPackageName); |
| expect(parsed.packageName).toBe(`${scope}/${name}`); |
| expect(parsed.scope).toBe(scope); |
| expect(parsed.version).toBe(version); |
| }); |
| }); |
|
|
| describe('executeCommand()', () => { |
| beforeEach(() => { |
| mocked(exec).mockImplementation(execMock); |
| }); |
|
|
| test('should call command with valid options', async () => { |
| const execMock = ((...args) => { |
| const arg = args[1] as ExecOptions; |
| expect(arg.cwd).toBeDefined(); |
| expect(arg.env).toBeDefined(); |
| |
| const cb = args[args.length - 1] as ExecCallback; |
| cb(null, 'Done', ''); |
| }) as typeof exec; |
|
|
| mocked(exec).mockImplementation(execMock); |
|
|
| await communityPackagesService.executeNpmCommand('ls'); |
|
|
| expect(exec).toHaveBeenCalled(); |
| }); |
|
|
| test('should make sure folder exists', async () => { |
| mocked(exec).mockImplementation(execMock); |
|
|
| await communityPackagesService.executeNpmCommand('ls'); |
|
|
| expect(exec).toHaveBeenCalled(); |
| }); |
|
|
| test('should throw especial error when package is not found', async () => { |
| const erroringExecMock = ((...args) => { |
| const cb = args[args.length - 1] as ExecCallback; |
| const msg = `Something went wrong - ${NPM_COMMAND_TOKENS.NPM_PACKAGE_NOT_FOUND_ERROR}. Aborting.`; |
| cb(new Error(msg), '', ''); |
| }) as typeof exec; |
|
|
| mocked(exec).mockImplementation(erroringExecMock); |
|
|
| const call = async () => await communityPackagesService.executeNpmCommand('ls'); |
|
|
| await expect(call).rejects.toThrowError(RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND); |
|
|
| expect(exec).toHaveBeenCalled(); |
| }); |
| }); |
|
|
| describe('crossInformationPackage()', () => { |
| test('should return same list if availableUpdates is undefined', () => { |
| const fakePkgs = mockPackagePair(); |
|
|
| const crossedPkgs = communityPackagesService.matchPackagesWithUpdates(fakePkgs); |
|
|
| expect(crossedPkgs).toEqual(fakePkgs); |
| }); |
|
|
| test('should correctly match update versions for packages', () => { |
| const [pkgA, pkgB] = mockPackagePair(); |
|
|
| const updates: CommunityPackages.AvailableUpdates = { |
| [pkgA.packageName]: { |
| current: pkgA.installedVersion, |
| wanted: pkgA.installedVersion, |
| latest: '0.2.0', |
| location: pkgA.packageName, |
| }, |
| [pkgB.packageName]: { |
| current: pkgA.installedVersion, |
| wanted: pkgA.installedVersion, |
| latest: '0.3.0', |
| location: pkgA.packageName, |
| }, |
| }; |
|
|
| const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] = |
| communityPackagesService.matchPackagesWithUpdates([pkgA, pkgB], updates); |
|
|
| expect(crossedPkgA.updateAvailable).toBe('0.2.0'); |
| expect(crossedPkgB.updateAvailable).toBe('0.3.0'); |
| }); |
|
|
| test('should correctly match update versions for single package', () => { |
| const [pkgA, pkgB] = mockPackagePair(); |
|
|
| const updates: CommunityPackages.AvailableUpdates = { |
| [pkgB.packageName]: { |
| current: pkgA.installedVersion, |
| wanted: pkgA.installedVersion, |
| latest: '0.3.0', |
| location: pkgA.packageName, |
| }, |
| }; |
|
|
| const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] = |
| communityPackagesService.matchPackagesWithUpdates([pkgA, pkgB], updates); |
|
|
| expect(crossedPkgA.updateAvailable).toBeUndefined(); |
| expect(crossedPkgB.updateAvailable).toBe('0.3.0'); |
| }); |
| }); |
|
|
| describe('matchMissingPackages()', () => { |
| test('should not match failed packages that do not exist', () => { |
| const fakePkgs = mockPackagePair(); |
| setMissingPackages([ |
| `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0`, |
| `${NODE_PACKAGE_PREFIX}another-very-long-name-that-never-is-seen`, |
| ]); |
|
|
| const matchedPackages = communityPackagesService.matchMissingPackages(fakePkgs); |
|
|
| expect(matchedPackages).toEqual(fakePkgs); |
|
|
| const [first, second] = matchedPackages; |
|
|
| expect(first.failedLoading).toBeUndefined(); |
| expect(second.failedLoading).toBeUndefined(); |
| }); |
|
|
| test('should match failed packages that should be present', () => { |
| const [pkgA, pkgB] = mockPackagePair(); |
| setMissingPackages([ |
| `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0`, |
| `${pkgA.packageName}@${pkgA.installedVersion}`, |
| ]); |
|
|
| const [matchedPkgA, matchedPkgB] = communityPackagesService.matchMissingPackages([ |
| pkgA, |
| pkgB, |
| ]); |
|
|
| expect(matchedPkgA.failedLoading).toBe(true); |
| expect(matchedPkgB.failedLoading).toBeUndefined(); |
| }); |
|
|
| test('should match failed packages even if version is wrong', () => { |
| const [pkgA, pkgB] = mockPackagePair(); |
| setMissingPackages([ |
| `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0`, |
| `${pkgA.packageName}@123.456.789`, |
| ]); |
| const [matchedPkgA, matchedPkgB] = communityPackagesService.matchMissingPackages([ |
| pkgA, |
| pkgB, |
| ]); |
|
|
| expect(matchedPkgA.failedLoading).toBe(true); |
| expect(matchedPkgB.failedLoading).toBeUndefined(); |
| }); |
| }); |
|
|
| describe('checkNpmPackageStatus()', () => { |
| test('should call axios.post', async () => { |
| await communityPackagesService.checkNpmPackageStatus(mockPackageName()); |
|
|
| expect(axios.post).toHaveBeenCalled(); |
| }); |
|
|
| test('should not fail if request fails', async () => { |
| mocked(axios.post).mockImplementation(() => { |
| throw new Error('Something went wrong'); |
| }); |
|
|
| const result = await communityPackagesService.checkNpmPackageStatus(mockPackageName()); |
|
|
| expect(result.status).toBe(NPM_PACKAGE_STATUS_GOOD); |
| }); |
|
|
| test('should warn if package is banned', async () => { |
| mocked(axios.post).mockResolvedValue({ data: { status: 'Banned', reason: 'Not good' } }); |
|
|
| const result = (await communityPackagesService.checkNpmPackageStatus( |
| mockPackageName(), |
| )) as CommunityPackages.PackageStatusCheck; |
|
|
| expect(result.status).toBe('Banned'); |
| expect(result.reason).toBe('Not good'); |
| }); |
| }); |
|
|
| describe('hasPackageLoadedSuccessfully()', () => { |
| test('should return true when failed package list does not exist', () => { |
| setMissingPackages([]); |
| expect(communityPackagesService.hasPackageLoaded('package')).toBe(true); |
| }); |
|
|
| test('should return true when package is not in the list of missing packages', () => { |
| setMissingPackages(['packageA@0.1.0', 'packageB@0.1.0']); |
| expect(communityPackagesService.hasPackageLoaded('packageC')).toBe(true); |
| }); |
|
|
| test('should return false when package is in the list of missing packages', () => { |
| setMissingPackages(['packageA@0.1.0', 'packageB@0.1.0']); |
| expect(communityPackagesService.hasPackageLoaded('packageA')).toBe(false); |
| }); |
| }); |
|
|
| describe('removePackageFromMissingList()', () => { |
| test('should do nothing if key does not exist', () => { |
| setMissingPackages([]); |
| communityPackagesService.removePackageFromMissingList('packageA'); |
|
|
| expect(communityPackagesService.missingPackages).toBeEmptyArray(); |
| }); |
|
|
| test('should remove only correct package from list', () => { |
| setMissingPackages(['packageA@0.1.0', 'packageB@0.2.0', 'packageC@0.2.0']); |
|
|
| communityPackagesService.removePackageFromMissingList('packageB'); |
|
|
| expect(communityPackagesService.missingPackages).toEqual([ |
| 'packageA@0.1.0', |
| 'packageC@0.2.0', |
| ]); |
| }); |
|
|
| test('should not remove if package is not in the list', () => { |
| const failedToLoadList = ['packageA@0.1.0', 'packageB@0.2.0', 'packageB@0.2.0']; |
| setMissingPackages(failedToLoadList); |
| communityPackagesService.removePackageFromMissingList('packageC'); |
|
|
| expect(communityPackagesService.missingPackages).toEqual(failedToLoadList); |
| }); |
| }); |
|
|
| const setMissingPackages = (missingPackages: string[]) => { |
| Object.assign(communityPackagesService, { missingPackages }); |
| }; |
|
|
| describe('updatePackage', () => { |
| const PACKAGE_NAME = 'n8n-nodes-test'; |
| const installedPackageForUpdateTest = mock<InstalledPackages>({ |
| packageName: PACKAGE_NAME, |
| }); |
|
|
| const packageDirectoryLoader = mock<PackageDirectoryLoader>({ |
| loadedNodes: [{ name: 'a-node-from-the-loader', version: 1 }], |
| }); |
|
|
| const testBlockDownloadDir = instanceSettings.nodesDownloadDir; |
| const testBlockPackageDir = `${testBlockDownloadDir}/node_modules/${PACKAGE_NAME}`; |
| const testBlockTarballName = `${PACKAGE_NAME}-latest.tgz`; |
| const testBlockRegistry = globalConfig.nodes.communityPackages.registry; |
| const testBlockNpmInstallArgs = [ |
| '--audit=false', |
| '--fund=false', |
| '--bin-links=false', |
| '--install-strategy=shallow', |
| '--ignore-scripts=true', |
| '--package-lock=false', |
| `--registry=${testBlockRegistry}`, |
| ].join(' '); |
|
|
| const execMockForThisBlock = (command: string, optionsOrCallback: any, callback?: any) => { |
| const actualCallback = typeof optionsOrCallback === 'function' ? optionsOrCallback : callback; |
| if (command.startsWith('npm pack') && command.includes(PACKAGE_NAME)) { |
| actualCallback(null, { stdout: testBlockTarballName, stderr: '' }); |
| } else { |
| actualCallback(null, 'Done', ''); |
| } |
| }; |
|
|
| beforeEach(() => { |
| jest.clearAllMocks(); |
|
|
| mocked(exec).mockImplementation(execMockForThisBlock as typeof exec); |
|
|
| mocked(readFile).mockResolvedValue( |
| JSON.stringify({ |
| name: PACKAGE_NAME, |
| version: '1.0.0', |
| dependencies: { 'some-actual-dep': '1.2.3' }, |
| devDependencies: { 'a-dev-dep': '1.0.0' }, |
| peerDependencies: { 'a-peer-dep': '2.0.0' }, |
| optionalDependencies: { 'an-optional-dep': '3.0.0' }, |
| }), |
| ); |
| mocked(writeFile).mockResolvedValue(undefined); |
|
|
| loadNodesAndCredentials.loadPackage.mockResolvedValue(packageDirectoryLoader); |
| loadNodesAndCredentials.unloadPackage.mockResolvedValue(undefined); |
| loadNodesAndCredentials.postProcessLoaders.mockResolvedValue(undefined); |
|
|
| installedPackageRepository.remove.mockResolvedValue(undefined as any); |
| installedPackageRepository.saveInstalledPackageWithNodes.mockResolvedValue( |
| installedPackageForUpdateTest, |
| ); |
|
|
| publisher.publishCommand.mockResolvedValue(undefined); |
| }); |
|
|
| test('should call `exec` with the correct sequence of commands, handle file ops, and interact with services', async () => { |
| |
| license.isCustomNpmRegistryEnabled.mockReturnValue(true); |
|
|
| |
| await communityPackagesService.updatePackage( |
| installedPackageForUpdateTest.packageName, |
| installedPackageForUpdateTest, |
| ); |
|
|
| |
| expect(rm).toHaveBeenCalledTimes(2); |
| expect(rm).toHaveBeenNthCalledWith(1, testBlockPackageDir, { recursive: true, force: true }); |
| expect(rm).toHaveBeenNthCalledWith(2, `${nodesDownloadDir}/n8n-nodes-test-latest.tgz`); |
|
|
| expect(exec).toHaveBeenCalledTimes(3); |
| expect(exec).toHaveBeenNthCalledWith( |
| 1, |
| `npm pack ${PACKAGE_NAME}@latest --registry=${testBlockRegistry} --quiet`, |
| { cwd: testBlockDownloadDir }, |
| expect.any(Function), |
| ); |
|
|
| expect(exec).toHaveBeenNthCalledWith( |
| 2, |
| `tar -xzf ${testBlockTarballName} -C ${testBlockPackageDir} --strip-components=1`, |
| { cwd: testBlockDownloadDir }, |
| expect.any(Function), |
| ); |
|
|
| expect(exec).toHaveBeenNthCalledWith( |
| 3, |
| `npm install ${testBlockNpmInstallArgs}`, |
| { cwd: testBlockPackageDir }, |
| expect.any(Function), |
| ); |
|
|
| expect(mkdir).toHaveBeenCalledWith(testBlockPackageDir, { recursive: true }); |
| expect(readFile).toHaveBeenCalledWith(`${testBlockPackageDir}/package.json`, 'utf-8'); |
| expect(writeFile).toHaveBeenCalledWith( |
| `${testBlockPackageDir}/package.json`, |
| JSON.stringify( |
| { |
| name: PACKAGE_NAME, |
| version: '1.0.0', |
| dependencies: { 'some-actual-dep': '1.2.3' }, |
| }, |
| null, |
| 2, |
| ), |
| 'utf-8', |
| ); |
|
|
| expect(loadNodesAndCredentials.unloadPackage).toHaveBeenCalledWith(PACKAGE_NAME); |
| expect(loadNodesAndCredentials.loadPackage).toHaveBeenCalledWith(PACKAGE_NAME); |
| expect(loadNodesAndCredentials.postProcessLoaders).toHaveBeenCalledTimes(1); |
|
|
| expect(installedPackageRepository.remove).toHaveBeenCalledWith(installedPackageForUpdateTest); |
| expect(installedPackageRepository.saveInstalledPackageWithNodes).toHaveBeenCalledWith( |
| packageDirectoryLoader, |
| ); |
|
|
| expect(publisher.publishCommand).toHaveBeenCalledWith({ |
| command: 'community-package-update', |
| payload: { packageName: PACKAGE_NAME, packageVersion: 'latest' }, |
| }); |
| }); |
|
|
| test('should throw when not licensed for custom registry if custom registry is different from default', async () => { |
| |
| license.isCustomNpmRegistryEnabled.mockReturnValue(false); |
|
|
| |
| await expect( |
| communityPackagesService.updatePackage( |
| installedPackageForUpdateTest.packageName, |
| installedPackageForUpdateTest, |
| ), |
| ).rejects.toThrow( |
| new FeatureNotLicensedError(LICENSE_FEATURES.COMMUNITY_NODES_CUSTOM_REGISTRY), |
| ); |
| }); |
| }); |
|
|
| describe('installPackage', () => { |
| test('should throw when installation of not vetted packages is forbidden', async () => { |
| globalConfig.nodes.communityPackages.unverifiedEnabled = false; |
| globalConfig.nodes.communityPackages.registry = 'https://registry.npmjs.org'; |
| await expect(communityPackagesService.installPackage('package', '0.1.0')).rejects.toThrow( |
| 'Installation of unverified community packages is forbidden!', |
| ); |
| }); |
| }); |
|
|
| describe('ensurePackageJson', () => { |
| const packageJsonPath = join(nodesDownloadDir, 'package.json'); |
|
|
| test('should not create package.json if it already exists', async () => { |
| mocked(access).mockResolvedValue(undefined); |
|
|
| await communityPackagesService.ensurePackageJson(); |
|
|
| expect(access).toHaveBeenCalledWith(packageJsonPath, constants.F_OK); |
| expect(mkdir).not.toHaveBeenCalled(); |
| expect(writeFile).not.toHaveBeenCalled(); |
| }); |
|
|
| test('should create package.json if it does not exist', async () => { |
| mocked(access).mockRejectedValue(new Error('ENOENT')); |
|
|
| await communityPackagesService.ensurePackageJson(); |
|
|
| expect(access).toHaveBeenCalledWith(packageJsonPath, constants.F_OK); |
| expect(mkdir).toHaveBeenCalledWith(nodesDownloadDir, { recursive: true }); |
| expect(writeFile).toHaveBeenCalledWith( |
| packageJsonPath, |
| JSON.stringify( |
| { |
| name: 'installed-nodes', |
| private: true, |
| dependencies: {}, |
| }, |
| null, |
| 2, |
| ), |
| 'utf-8', |
| ); |
| }); |
| }); |
|
|
| describe('checkForMissingPackages', () => { |
| const installedPackage1 = mock<InstalledPackages>({ |
| packageName: 'package-1', |
| installedVersion: '1.0.0', |
| installedNodes: [{ type: 'node-type-1' }], |
| }); |
| const installedPackage2 = mock<InstalledPackages>({ |
| packageName: 'package-2', |
| installedVersion: '2.0.0', |
| installedNodes: [{ type: 'node-type-2' }], |
| }); |
|
|
| beforeEach(() => { |
| jest |
| .spyOn(communityPackagesService, 'installPackage') |
| .mockResolvedValue({} as InstalledPackages); |
| }); |
|
|
| test('should set missingPackages to empty array when no packages are missing', async () => { |
| const installedPackages = [installedPackage1]; |
|
|
| installedPackageRepository.find.mockResolvedValue(installedPackages); |
| loadNodesAndCredentials.isKnownNode.mockReturnValue(true); |
|
|
| await communityPackagesService.checkForMissingPackages(); |
|
|
| expect(communityPackagesService.missingPackages).toEqual([]); |
| expect(communityPackagesService.installPackage).not.toHaveBeenCalled(); |
| expect(loadNodesAndCredentials.postProcessLoaders).not.toHaveBeenCalled(); |
| }); |
|
|
| test('should identify missing packages without reinstalling when reinstallMissing is false', async () => { |
| const installedPackages = [installedPackage1, installedPackage2]; |
|
|
| installedPackageRepository.find.mockResolvedValue(installedPackages); |
| loadNodesAndCredentials.isKnownNode.mockImplementation( |
| (nodeType) => nodeType === 'node-type-2', |
| ); |
| globalConfig.nodes.communityPackages.reinstallMissing = false; |
|
|
| await communityPackagesService.checkForMissingPackages(); |
|
|
| expect(communityPackagesService.missingPackages).toEqual(['package-1@1.0.0']); |
| expect(communityPackagesService.installPackage).not.toHaveBeenCalled(); |
| expect(loadNodesAndCredentials.postProcessLoaders).not.toHaveBeenCalled(); |
| expect(logger.warn).toHaveBeenCalled(); |
| }); |
|
|
| test('should reinstall missing packages when reinstallMissing is true', async () => { |
| const installedPackages = [installedPackage1]; |
|
|
| installedPackageRepository.find.mockResolvedValue(installedPackages); |
| loadNodesAndCredentials.isKnownNode.mockReturnValue(false); |
| globalConfig.nodes.communityPackages.reinstallMissing = true; |
|
|
| await communityPackagesService.checkForMissingPackages(); |
|
|
| expect(communityPackagesService.installPackage).toHaveBeenCalledWith('package-1', '1.0.0'); |
| expect(loadNodesAndCredentials.postProcessLoaders).toHaveBeenCalled(); |
| expect(communityPackagesService.missingPackages).toEqual([]); |
| expect(logger.info).toHaveBeenCalledWith( |
| 'Packages reinstalled successfully. Resuming regular initialization.', |
| ); |
| }); |
|
|
| test('should handle failed reinstallations and record missing packages', async () => { |
| const installedPackages = [installedPackage1]; |
|
|
| installedPackageRepository.find.mockResolvedValue(installedPackages); |
| loadNodesAndCredentials.isKnownNode.mockReturnValue(false); |
| globalConfig.nodes.communityPackages.reinstallMissing = true; |
| communityPackagesService.installPackage = jest |
| .fn() |
| .mockRejectedValue(new Error('Installation failed')); |
|
|
| await communityPackagesService.checkForMissingPackages(); |
|
|
| expect(communityPackagesService.installPackage).toHaveBeenCalledWith('package-1', '1.0.0'); |
| expect(logger.error).toHaveBeenCalledWith('n8n was unable to install the missing packages.'); |
| expect(communityPackagesService.missingPackages).toEqual(['package-1@1.0.0']); |
| }); |
|
|
| test('should handle multiple missing packages and stop reinstalling after first failure', async () => { |
| const installedPackages = [installedPackage1, installedPackage2]; |
|
|
| installedPackageRepository.find.mockResolvedValue(installedPackages); |
| loadNodesAndCredentials.isKnownNode.mockReturnValue(false); |
| globalConfig.nodes.communityPackages.reinstallMissing = true; |
|
|
| |
| communityPackagesService.installPackage = jest |
| .fn() |
| .mockResolvedValueOnce({} as InstalledPackages) |
| .mockRejectedValueOnce(new Error('Installation failed')); |
|
|
| await communityPackagesService.checkForMissingPackages(); |
|
|
| expect(communityPackagesService.installPackage).toHaveBeenCalledWith('package-1', '1.0.0'); |
| expect(communityPackagesService.installPackage).toHaveBeenCalledWith('package-2', '2.0.0'); |
| expect(logger.error).toHaveBeenCalledWith('n8n was unable to install the missing packages.'); |
| expect(communityPackagesService.missingPackages).toEqual(['package-2@2.0.0']); |
| }); |
| }); |
|
|
| describe('updatePackageJsonDependency', () => { |
| beforeEach(() => { |
| jest.clearAllMocks(); |
| mocked(readFile).mockResolvedValue(JSON.stringify({ dependencies: {} })); |
| }); |
|
|
| test('should update package dependencies', async () => { |
| await communityPackagesService.updatePackageJsonDependency('test-package', '1.0.0'); |
|
|
| expect(writeFile).toHaveBeenCalledWith( |
| `${nodesDownloadDir}/package.json`, |
| JSON.stringify({ dependencies: { 'test-package': '1.0.0' } }, null, 2), |
| 'utf-8', |
| ); |
| }); |
|
|
| test('should create file and update package dependencies', async () => { |
| await communityPackagesService.updatePackageJsonDependency('test-package', '1.0.0'); |
|
|
| expect(writeFile).toHaveBeenCalledWith( |
| `${nodesDownloadDir}/package.json`, |
| JSON.stringify({ dependencies: { 'test-package': '1.0.0' } }, null, 2), |
| 'utf-8', |
| ); |
| }); |
| }); |
| }); |
|
|