refactor(core): Refactor nodes loading (no-changelog) (#7283)

fixes PAY-605
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2023-10-09 16:09:23 +02:00
committed by GitHub
parent 789e1e7ed4
commit c5ee06cc61
31 changed files with 603 additions and 683 deletions

View File

@@ -8,7 +8,7 @@ import { toReportTitle } from '@/audit/utils';
import { mockInstance } from '../shared/utils/';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { NodeTypes } from '@/NodeTypes';
import { CommunityPackageService } from '@/services/communityPackage.service';
import { CommunityPackagesService } from '@/services/communityPackages.service';
import Container from 'typedi';
import { LoggerProxy } from 'n8n-workflow';
@@ -19,8 +19,8 @@ LoggerProxy.init(getLogger());
const nodesAndCredentials = mockInstance(LoadNodesAndCredentials);
nodesAndCredentials.getCustomDirectories.mockReturnValue([]);
mockInstance(NodeTypes);
const communityPackageService = mockInstance(CommunityPackageService);
Container.set(CommunityPackageService, communityPackageService);
const communityPackagesService = mockInstance(CommunityPackagesService);
Container.set(CommunityPackagesService, communityPackagesService);
beforeAll(async () => {
await testDb.init();
@@ -36,7 +36,7 @@ afterAll(async () => {
});
test('should report risky official nodes', async () => {
communityPackageService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
communityPackagesService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
const map = [...OFFICIAL_RISKY_NODE_TYPES].reduce<{ [nodeType: string]: string }>((acc, cur) => {
return (acc[cur] = uuid()), acc;
}, {});
@@ -81,7 +81,7 @@ test('should report risky official nodes', async () => {
});
test('should not report non-risky official nodes', async () => {
communityPackageService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
communityPackagesService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
await saveManualTriggerWorkflow();
const testAudit = await audit(['nodes']);
@@ -96,7 +96,7 @@ test('should not report non-risky official nodes', async () => {
});
test('should report community nodes', async () => {
communityPackageService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
communityPackagesService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
const testAudit = await audit(['nodes']);

View File

@@ -1,16 +1,18 @@
import * as testDb from '../shared/testDb';
import { mockInstance } from '../shared/utils/';
import * as Config from '@oclif/config';
import { mock } from 'jest-mock-extended';
import { type ILogger, LoggerProxy } from 'n8n-workflow';
import { InternalHooks } from '@/InternalHooks';
import { ImportWorkflowsCommand } from '@/commands/import/workflow';
import * as Config from '@oclif/config';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import * as testDb from '../shared/testDb';
import { mockInstance } from '../shared/utils/';
import { LoggerProxy } from 'n8n-workflow';
import { getLogger } from '@/Logger';
LoggerProxy.init(getLogger());
LoggerProxy.init(mock<ILogger>());
beforeAll(async () => {
mockInstance(InternalHooks);
mockInstance(LoadNodesAndCredentials);
await testDb.init();
});

View File

@@ -1,8 +1,11 @@
import path from 'path';
import type { SuperAgentTest } from 'supertest';
import type { InstalledPackages } from '@db/entities/InstalledPackages';
import type { InstalledNodes } from '@db/entities/InstalledNodes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { Push } from '@/push';
import { CommunityPackageService } from '@/services/communityPackage.service';
import { CommunityPackagesService } from '@/services/communityPackages.service';
import { COMMUNITY_PACKAGE_VERSION } from './shared/constants';
import * as testDb from './shared/testDb';
@@ -14,15 +17,13 @@ import {
mockPackageName,
} from './shared/utils';
import type { InstalledPackages } from '@db/entities/InstalledPackages';
import type { InstalledNodes } from '@db/entities/InstalledNodes';
import type { SuperAgentTest } from 'supertest';
const communityPackageService = mockInstance(CommunityPackageService);
const mockLoadNodesAndCredentials = mockInstance(LoadNodesAndCredentials);
const communityPackagesService = mockInstance(CommunityPackagesService, {
hasMissingPackages: false,
});
mockInstance(LoadNodesAndCredentials);
mockInstance(Push);
const testServer = setupTestServer({ endpointGroups: ['nodes'] });
const testServer = setupTestServer({ endpointGroups: ['community-packages'] });
const commonUpdatesProps = {
createdAt: new Date(),
@@ -47,12 +48,12 @@ beforeEach(() => {
jest.resetAllMocks();
});
describe('GET /nodes', () => {
describe('GET /community-packages', () => {
test('should respond 200 if no nodes are installed', async () => {
communityPackageService.getAllInstalledPackages.mockResolvedValue([]);
communityPackagesService.getAllInstalledPackages.mockResolvedValue([]);
const {
body: { data },
} = await authAgent.get('/nodes').expect(200);
} = await authAgent.get('/community-packages').expect(200);
expect(data).toHaveLength(0);
});
@@ -61,12 +62,12 @@ describe('GET /nodes', () => {
const pkg = mockPackage();
const node = mockNode(pkg.packageName);
pkg.installedNodes = [node];
communityPackageService.getAllInstalledPackages.mockResolvedValue([pkg]);
communityPackageService.matchPackagesWithUpdates.mockReturnValue([pkg]);
communityPackagesService.getAllInstalledPackages.mockResolvedValue([pkg]);
communityPackagesService.matchPackagesWithUpdates.mockReturnValue([pkg]);
const {
body: { data },
} = await authAgent.get('/nodes').expect(200);
} = await authAgent.get('/community-packages').expect(200);
expect(data).toHaveLength(1);
expect(data[0].installedNodes).toHaveLength(1);
@@ -80,9 +81,9 @@ describe('GET /nodes', () => {
const nodeB = mockNode(pkgB.packageName);
const nodeC = mockNode(pkgB.packageName);
communityPackageService.getAllInstalledPackages.mockResolvedValue([pkgA, pkgB]);
communityPackagesService.getAllInstalledPackages.mockResolvedValue([pkgA, pkgB]);
communityPackageService.matchPackagesWithUpdates.mockReturnValue([
communityPackagesService.matchPackagesWithUpdates.mockReturnValue([
{
...commonUpdatesProps,
packageName: pkgA.packageName,
@@ -97,7 +98,7 @@ describe('GET /nodes', () => {
const {
body: { data },
} = await authAgent.get('/nodes').expect(200);
} = await authAgent.get('/community-packages').expect(200);
expect(data).toHaveLength(2);
@@ -110,26 +111,26 @@ describe('GET /nodes', () => {
});
test('should not check for updates if no packages installed', async () => {
await authAgent.get('/nodes');
await authAgent.get('/community-packages');
expect(communityPackageService.executeNpmCommand).not.toHaveBeenCalled();
expect(communityPackagesService.executeNpmCommand).not.toHaveBeenCalled();
});
test('should check for updates if packages installed', async () => {
communityPackageService.getAllInstalledPackages.mockResolvedValue([mockPackage()]);
communityPackagesService.getAllInstalledPackages.mockResolvedValue([mockPackage()]);
await authAgent.get('/nodes').expect(200);
await authAgent.get('/community-packages').expect(200);
const args = ['npm outdated --json', { doNotHandleError: true }];
expect(communityPackageService.executeNpmCommand).toHaveBeenCalledWith(...args);
expect(communityPackagesService.executeNpmCommand).toHaveBeenCalledWith(...args);
});
test('should report package updates if available', async () => {
const pkg = mockPackage();
communityPackageService.getAllInstalledPackages.mockResolvedValue([pkg]);
communityPackagesService.getAllInstalledPackages.mockResolvedValue([pkg]);
communityPackageService.executeNpmCommand.mockImplementation(() => {
communityPackagesService.executeNpmCommand.mockImplementation(() => {
throw {
code: 1,
stdout: JSON.stringify({
@@ -143,7 +144,7 @@ describe('GET /nodes', () => {
};
});
communityPackageService.matchPackagesWithUpdates.mockReturnValue([
communityPackagesService.matchPackagesWithUpdates.mockReturnValue([
{
packageName: 'test',
installedNodes: [],
@@ -153,7 +154,7 @@ describe('GET /nodes', () => {
const {
body: { data },
} = await authAgent.get('/nodes').expect(200);
} = await authAgent.get('/community-packages').expect(200);
const [returnedPkg] = data;
@@ -162,89 +163,92 @@ describe('GET /nodes', () => {
});
});
describe('POST /nodes', () => {
describe('POST /community-packages', () => {
test('should reject if package name is missing', async () => {
await authAgent.post('/nodes').expect(400);
await authAgent.post('/community-packages').expect(400);
});
test('should reject if package is duplicate', async () => {
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
communityPackageService.isPackageInstalled.mockResolvedValue(true);
communityPackageService.hasPackageLoaded.mockReturnValue(true);
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage());
communityPackagesService.isPackageInstalled.mockResolvedValue(true);
communityPackagesService.hasPackageLoaded.mockReturnValue(true);
communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
const {
body: { message },
} = await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(400);
} = await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(400);
expect(message).toContain('already installed');
});
test('should allow installing packages that could not be loaded', async () => {
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
communityPackageService.hasPackageLoaded.mockReturnValue(false);
communityPackageService.checkNpmPackageStatus.mockResolvedValue({ status: 'OK' });
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
mockLoadNodesAndCredentials.installNpmModule.mockResolvedValue(mockPackage());
communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage());
communityPackagesService.hasPackageLoaded.mockReturnValue(false);
communityPackagesService.checkNpmPackageStatus.mockResolvedValue({ status: 'OK' });
communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
communityPackagesService.installNpmModule.mockResolvedValue(mockPackage());
await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(200);
await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(200);
expect(communityPackageService.removePackageFromMissingList).toHaveBeenCalled();
expect(communityPackagesService.removePackageFromMissingList).toHaveBeenCalled();
});
test('should not install a banned package', async () => {
communityPackageService.checkNpmPackageStatus.mockResolvedValue({ status: 'Banned' });
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
communityPackagesService.checkNpmPackageStatus.mockResolvedValue({ status: 'Banned' });
communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
const {
body: { message },
} = await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(400);
} = await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(400);
expect(message).toContain('banned');
});
});
describe('DELETE /nodes', () => {
describe('DELETE /community-packages', () => {
test('should not delete if package name is empty', async () => {
await authAgent.delete('/nodes').expect(400);
await authAgent.delete('/community-packages').expect(400);
});
test('should reject if package is not installed', async () => {
const {
body: { message },
} = await authAgent.delete('/nodes').query({ name: mockPackageName() }).expect(400);
} = await authAgent
.delete('/community-packages')
.query({ name: mockPackageName() })
.expect(400);
expect(message).toContain('not installed');
});
test('should uninstall package', async () => {
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage());
await authAgent.delete('/nodes').query({ name: mockPackageName() }).expect(200);
await authAgent.delete('/community-packages').query({ name: mockPackageName() }).expect(200);
expect(mockLoadNodesAndCredentials.removeNpmModule).toHaveBeenCalledTimes(1);
expect(communityPackagesService.removeNpmModule).toHaveBeenCalledTimes(1);
});
});
describe('PATCH /nodes', () => {
describe('PATCH /community-packages', () => {
test('should reject if package name is empty', async () => {
await authAgent.patch('/nodes').expect(400);
await authAgent.patch('/community-packages').expect(400);
});
test('should reject if package is not installed', async () => {
const {
body: { message },
} = await authAgent.patch('/nodes').send({ name: mockPackageName() }).expect(400);
} = await authAgent.patch('/community-packages').send({ name: mockPackageName() }).expect(400);
expect(message).toContain('not installed');
});
test('should update a package', async () => {
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage());
communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
await authAgent.patch('/nodes').send({ name: mockPackageName() });
await authAgent.patch('/community-packages').send({ name: mockPackageName() });
expect(mockLoadNodesAndCredentials.updateNpmModule).toHaveBeenCalledTimes(1);
expect(communityPackagesService.updateNpmModule).toHaveBeenCalledTimes(1);
});
});

View File

@@ -22,7 +22,7 @@ export type EndpointGroup =
| 'credentials'
| 'workflows'
| 'publicApi'
| 'nodes'
| 'community-packages'
| 'ldap'
| 'saml'
| 'sourceControl'

View File

@@ -25,7 +25,6 @@ import {
LdapController,
MFAController,
MeController,
NodesController,
OwnerController,
PasswordResetController,
TagsController,
@@ -34,12 +33,10 @@ import {
import { rawBodyReader, bodyParser, setupAuthMiddlewares } from '@/middlewares';
import { InternalHooks } from '@/InternalHooks';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { PostHogClient } from '@/posthog';
import { variablesController } from '@/environments/variables/variables.controller';
import { LdapManager } from '@/Ldap/LdapManager.ee';
import { handleLdapInit } from '@/Ldap/helpers';
import { Push } from '@/push';
import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers';
import { SamlController } from '@/sso/saml/routes/saml.controller.ee';
import { EventBusController } from '@/eventbus/eventBus.controller';
@@ -242,17 +239,11 @@ export const setupTestServer = ({
case 'sourceControl':
registerController(app, config, Container.get(SourceControlController));
break;
case 'nodes':
registerController(
app,
config,
new NodesController(
config,
Container.get(LoadNodesAndCredentials),
Container.get(Push),
internalHooks,
),
case 'community-packages':
const { CommunityPackagesController } = await import(
'@/controllers/communityPackages.controller'
);
registerController(app, config, Container.get(CommunityPackagesController));
case 'me':
registerController(
app,

View File

@@ -1,7 +1,8 @@
import { v4 as uuid } from 'uuid';
import { mocked } from 'jest-mock';
import { Container } from 'typedi';
import type { ICredentialTypes, INode, INodesAndCredentials } from 'n8n-workflow';
import type { INode } from 'n8n-workflow';
import { LoggerProxy, NodeApiError, NodeOperationError, Workflow } from 'n8n-workflow';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
@@ -11,22 +12,19 @@ import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { Role } from '@db/entities/Role';
import { User } from '@db/entities/User';
import { getLogger } from '@/Logger';
import { randomEmail, randomName } from '../integration/shared/random';
import * as Helpers from './Helpers';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import { WorkflowRunner } from '@/WorkflowRunner';
import { mock } from 'jest-mock-extended';
import type { ExternalHooks } from '@/ExternalHooks';
import { Container } from 'typedi';
import { ExternalHooks } from '@/ExternalHooks';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { mockInstance } from '../integration/shared/utils/';
import { Push } from '@/push';
import { ActiveExecutions } from '@/ActiveExecutions';
import { NodeTypes } from '@/NodeTypes';
import { SecretsHelper } from '@/SecretsHelpers';
import { WebhookService } from '@/services/webhook.service';
import { VariablesService } from '../../src/environments/variables/variables.service';
import { VariablesService } from '@/environments/variables/variables.service';
import { mockInstance } from '../integration/shared/utils/';
import { randomEmail, randomName } from '../integration/shared/random';
import * as Helpers from './Helpers';
/**
* TODO:
@@ -114,13 +112,6 @@ jest.mock('@/Db', () => {
return fakeQueryBuilder;
}),
},
Webhook: {
clear: jest.fn(),
delete: jest.fn(),
},
Variables: {
find: jest.fn(() => []),
},
},
};
});
@@ -140,37 +131,24 @@ const workflowExecuteAdditionalDataExecuteErrorWorkflowSpy = jest.spyOn(
);
describe('ActiveWorkflowRunner', () => {
let externalHooks: ExternalHooks;
let activeWorkflowRunner: ActiveWorkflowRunner;
mockInstance(ActiveExecutions);
const externalHooks = mockInstance(ExternalHooks);
const webhookService = mockInstance(WebhookService);
mockInstance(Push);
mockInstance(SecretsHelper);
const variablesService = mockInstance(VariablesService);
const nodesAndCredentials = mockInstance(LoadNodesAndCredentials);
Object.assign(nodesAndCredentials, {
loadedNodes: MOCK_NODE_TYPES_DATA,
known: { nodes: {}, credentials: {} },
types: { nodes: [], credentials: [] },
});
const activeWorkflowRunner = Container.get(ActiveWorkflowRunner);
beforeAll(async () => {
LoggerProxy.init(getLogger());
const nodesAndCredentials: INodesAndCredentials = {
loaded: {
nodes: MOCK_NODE_TYPES_DATA,
credentials: {},
},
known: { nodes: {}, credentials: {} },
credentialTypes: {} as ICredentialTypes,
};
const mockVariablesService = {
getAllCached: jest.fn(() => []),
};
Container.set(LoadNodesAndCredentials, nodesAndCredentials);
Container.set(VariablesService, mockVariablesService);
mockInstance(Push);
mockInstance(SecretsHelper);
});
beforeEach(() => {
externalHooks = mock();
activeWorkflowRunner = new ActiveWorkflowRunner(
new ActiveExecutions(),
externalHooks,
Container.get(NodeTypes),
webhookService,
);
variablesService.getAllCached.mockResolvedValue([]);
});
afterEach(async () => {

View File

@@ -1,36 +1,29 @@
import type { ICredentialTypes, INodesAndCredentials } from 'n8n-workflow';
import { CredentialTypes } from '@/CredentialTypes';
import { Container } from 'typedi';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { mockInstance } from '../integration/shared/utils';
describe('CredentialTypes', () => {
const mockNodesAndCredentials: INodesAndCredentials = {
loaded: {
nodes: {},
credentials: {
fakeFirstCredential: {
type: {
name: 'fakeFirstCredential',
displayName: 'Fake First Credential',
properties: [],
},
sourcePath: '',
const mockNodesAndCredentials = mockInstance(LoadNodesAndCredentials, {
loadedCredentials: {
fakeFirstCredential: {
type: {
name: 'fakeFirstCredential',
displayName: 'Fake First Credential',
properties: [],
},
fakeSecondCredential: {
type: {
name: 'fakeSecondCredential',
displayName: 'Fake Second Credential',
properties: [],
},
sourcePath: '',
sourcePath: '',
},
fakeSecondCredential: {
type: {
name: 'fakeSecondCredential',
displayName: 'Fake Second Credential',
properties: [],
},
sourcePath: '',
},
},
known: { nodes: {}, credentials: {} },
credentialTypes: {} as ICredentialTypes,
};
Container.set(LoadNodesAndCredentials, mockNodesAndCredentials);
});
const credentialTypes = Container.get(CredentialTypes);
@@ -39,7 +32,7 @@ describe('CredentialTypes', () => {
});
test('Should return correct credential type for valid name', () => {
const mockedCredentialTypes = mockNodesAndCredentials.loaded.credentials;
const mockedCredentialTypes = mockNodesAndCredentials.loadedCredentials;
expect(credentialTypes.getByName('fakeFirstCredential')).toStrictEqual(
mockedCredentialTypes.fakeFirstCredential.type,
);

View File

@@ -2,68 +2,58 @@ import type {
IAuthenticateGeneric,
ICredentialDataDecryptedObject,
ICredentialType,
ICredentialTypes,
IHttpRequestOptions,
INode,
INodeProperties,
INodesAndCredentials,
} from 'n8n-workflow';
import { deepCopy } from 'n8n-workflow';
import { Workflow } from 'n8n-workflow';
import { CredentialsHelper } from '@/CredentialsHelper';
import { CredentialTypes } from '@/CredentialTypes';
import { Container } from 'typedi';
import { NodeTypes } from '@/NodeTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { mockInstance } from '../integration/shared/utils';
describe('CredentialsHelper', () => {
const TEST_ENCRYPTION_KEY = 'test';
const mockNodesAndCredentials: INodesAndCredentials = {
loaded: {
nodes: {
'test.set': {
sourcePath: '',
type: {
description: {
displayName: 'Set',
name: 'set',
group: ['input'],
version: 1,
description: 'Sets a value',
defaults: {
name: 'Set',
color: '#0000FF',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Value1',
name: 'value1',
type: 'string',
default: 'default-value1',
},
{
displayName: 'Value2',
name: 'value2',
type: 'string',
default: 'default-value2',
},
],
const mockNodesAndCredentials = mockInstance(LoadNodesAndCredentials, {
loadedNodes: {
'test.set': {
sourcePath: '',
type: {
description: {
displayName: 'Set',
name: 'set',
group: ['input'],
version: 1,
description: 'Sets a value',
defaults: {
name: 'Set',
color: '#0000FF',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Value1',
name: 'value1',
type: 'string',
default: 'default-value1',
},
{
displayName: 'Value2',
name: 'value2',
type: 'string',
default: 'default-value2',
},
],
},
},
},
credentials: {},
},
known: { nodes: {}, credentials: {} },
credentialTypes: {} as ICredentialTypes,
};
});
Container.set(LoadNodesAndCredentials, mockNodesAndCredentials);
const nodeTypes = Container.get(NodeTypes);
const nodeTypes = mockInstance(NodeTypes);
describe('authenticate', () => {
const tests: Array<{
@@ -280,20 +270,14 @@ describe('CredentialsHelper', () => {
for (const testData of tests) {
test(testData.description, async () => {
mockNodesAndCredentials.loaded.credentials = {
mockNodesAndCredentials.loadedCredentials = {
[testData.input.credentialType.name]: {
type: testData.input.credentialType,
sourcePath: '',
},
};
const credentialTypes = Container.get(CredentialTypes);
const credentialsHelper = new CredentialsHelper(
TEST_ENCRYPTION_KEY,
credentialTypes,
nodeTypes,
);
const credentialsHelper = new CredentialsHelper(TEST_ENCRYPTION_KEY);
const result = await credentialsHelper.authenticate(
testData.input.credentials,

View File

@@ -1,7 +1,8 @@
import { v4 as uuid } from 'uuid';
import { Container } from 'typedi';
import type { ICredentialTypes, INodeTypes } from 'n8n-workflow';
import { SubworkflowOperationError, Workflow } from 'n8n-workflow';
import { mock } from 'jest-mock-extended';
import type { ILogger, INodeTypes } from 'n8n-workflow';
import { LoggerProxy, SubworkflowOperationError, Workflow } from 'n8n-workflow';
import config from '@/config';
import * as Db from '@/Db';
@@ -14,35 +15,26 @@ import { UserService } from '@/services/user.service';
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
import * as UserManagementHelper from '@/UserManagement/UserManagementHelper';
import { WorkflowsService } from '@/workflows/workflows.services';
import { OwnershipService } from '@/services/ownership.service';
import { mockInstance } from '../integration/shared/utils/';
import {
randomCredentialPayload as randomCred,
randomPositiveDigit,
} from '../integration/shared/random';
import * as testDb from '../integration/shared/testDb';
import { mockNodeTypesData } from './Helpers';
import type { SaveCredentialFunction } from '../integration/shared/types';
import { mockInstance } from '../integration/shared/utils/';
import { OwnershipService } from '@/services/ownership.service';
import { mockNodeTypesData } from './Helpers';
import { LoggerProxy } from 'n8n-workflow';
import { getLogger } from '@/Logger';
LoggerProxy.init(getLogger());
LoggerProxy.init(mock<ILogger>());
let mockNodeTypes: INodeTypes;
let credentialOwnerRole: Role;
let workflowOwnerRole: Role;
let saveCredential: SaveCredentialFunction;
const MOCK_NODE_TYPES_DATA = mockNodeTypesData(['start', 'actionNetwork']);
mockInstance(LoadNodesAndCredentials, {
loaded: {
nodes: MOCK_NODE_TYPES_DATA,
credentials: {},
},
known: { nodes: {}, credentials: {} },
credentialTypes: {} as ICredentialTypes,
loadedNodes: mockNodeTypesData(['start', 'actionNetwork']),
});
beforeAll(async () => {

View File

@@ -1,7 +1,9 @@
import { exec } from 'child_process';
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';
import axios from 'axios';
import { mocked } from 'jest-mock';
import Container from 'typedi';
import type { PublicInstalledPackage } from 'n8n-workflow';
import {
NODE_PACKAGE_PREFIX,
@@ -9,22 +11,20 @@ import {
NPM_PACKAGE_STATUS_GOOD,
RESPONSE_ERROR_MESSAGES,
} from '@/constants';
import { InstalledPackages } from '@db/entities/InstalledPackages';
import { randomName } from '../../integration/shared/random';
import config from '@/config';
import { mockInstance, mockPackageName, mockPackagePair } from '../../integration/shared/utils';
import { mocked } from 'jest-mock';
import { InstalledPackages } from '@db/entities/InstalledPackages';
import type { CommunityPackages } from '@/Interfaces';
import { CommunityPackageService } from '@/services/communityPackage.service';
import { CommunityPackagesService } from '@/services/communityPackages.service';
import { InstalledNodesRepository, InstalledPackagesRepository } from '@/databases/repositories';
import Container from 'typedi';
import { InstalledNodes } from '@/databases/entities/InstalledNodes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import {
COMMUNITY_NODE_VERSION,
COMMUNITY_PACKAGE_VERSION,
} from '../../integration/shared/constants';
import type { PublicInstalledPackage } from 'n8n-workflow';
import { randomName } from '../../integration/shared/random';
import { mockInstance, mockPackageName, mockPackagePair } from '../../integration/shared/utils';
jest.mock('fs/promises');
jest.mock('child_process');
@@ -38,10 +38,8 @@ const execMock = ((...args) => {
cb(null, 'Done', '');
}) as typeof exec;
describe('CommunityPackageService', () => {
describe('CommunityPackagesService', () => {
const installedNodesRepository = mockInstance(InstalledNodesRepository);
Container.set(InstalledNodesRepository, installedNodesRepository);
installedNodesRepository.create.mockImplementation(() => {
const nodeName = randomName();
@@ -54,7 +52,6 @@ describe('CommunityPackageService', () => {
});
const installedPackageRepository = mockInstance(InstalledPackagesRepository);
installedPackageRepository.create.mockImplementation(() => {
return Object.assign(new InstalledPackages(), {
packageName: mockPackageName(),
@@ -62,7 +59,9 @@ describe('CommunityPackageService', () => {
});
});
const communityPackageService = new CommunityPackageService(installedPackageRepository);
mockInstance(LoadNodesAndCredentials);
const communityPackagesService = Container.get(CommunityPackagesService);
beforeEach(() => {
config.load(config.default);
@@ -70,18 +69,18 @@ describe('CommunityPackageService', () => {
describe('parseNpmPackageName()', () => {
test('should fail with empty package name', () => {
expect(() => communityPackageService.parseNpmPackageName('')).toThrowError();
expect(() => communityPackagesService.parseNpmPackageName('')).toThrowError();
});
test('should fail with invalid package prefix name', () => {
expect(() =>
communityPackageService.parseNpmPackageName('INVALID_PREFIX@123'),
communityPackagesService.parseNpmPackageName('INVALID_PREFIX@123'),
).toThrowError();
});
test('should parse valid package name', () => {
const name = mockPackageName();
const parsed = communityPackageService.parseNpmPackageName(name);
const parsed = communityPackagesService.parseNpmPackageName(name);
expect(parsed.rawString).toBe(name);
expect(parsed.packageName).toBe(name);
@@ -93,7 +92,7 @@ describe('CommunityPackageService', () => {
const name = mockPackageName();
const version = '0.1.1';
const fullPackageName = `${name}@${version}`;
const parsed = communityPackageService.parseNpmPackageName(fullPackageName);
const parsed = communityPackagesService.parseNpmPackageName(fullPackageName);
expect(parsed.rawString).toBe(fullPackageName);
expect(parsed.packageName).toBe(name);
@@ -106,7 +105,7 @@ describe('CommunityPackageService', () => {
const name = mockPackageName();
const version = '0.1.1';
const fullPackageName = `${scope}/${name}@${version}`;
const parsed = communityPackageService.parseNpmPackageName(fullPackageName);
const parsed = communityPackagesService.parseNpmPackageName(fullPackageName);
expect(parsed.rawString).toBe(fullPackageName);
expect(parsed.packageName).toBe(`${scope}/${name}`);
@@ -134,7 +133,7 @@ describe('CommunityPackageService', () => {
mocked(exec).mockImplementation(execMock);
await communityPackageService.executeNpmCommand('ls');
await communityPackagesService.executeNpmCommand('ls');
expect(fsAccess).toHaveBeenCalled();
expect(exec).toHaveBeenCalled();
@@ -144,7 +143,7 @@ describe('CommunityPackageService', () => {
test('should make sure folder exists', async () => {
mocked(exec).mockImplementation(execMock);
await communityPackageService.executeNpmCommand('ls');
await communityPackagesService.executeNpmCommand('ls');
expect(fsAccess).toHaveBeenCalled();
expect(exec).toHaveBeenCalled();
expect(fsMkdir).toBeCalledTimes(0);
@@ -156,7 +155,7 @@ describe('CommunityPackageService', () => {
throw new Error('Folder does not exist.');
});
await communityPackageService.executeNpmCommand('ls');
await communityPackagesService.executeNpmCommand('ls');
expect(fsAccess).toHaveBeenCalled();
expect(exec).toHaveBeenCalled();
@@ -172,7 +171,7 @@ describe('CommunityPackageService', () => {
mocked(exec).mockImplementation(erroringExecMock);
const call = async () => communityPackageService.executeNpmCommand('ls');
const call = async () => communityPackagesService.executeNpmCommand('ls');
await expect(call).rejects.toThrowError(RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND);
@@ -186,7 +185,7 @@ describe('CommunityPackageService', () => {
test('should return same list if availableUpdates is undefined', () => {
const fakePkgs = mockPackagePair();
const crossedPkgs = communityPackageService.matchPackagesWithUpdates(fakePkgs);
const crossedPkgs = communityPackagesService.matchPackagesWithUpdates(fakePkgs);
expect(crossedPkgs).toEqual(fakePkgs);
});
@@ -210,7 +209,7 @@ describe('CommunityPackageService', () => {
};
const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] =
communityPackageService.matchPackagesWithUpdates([pkgA, pkgB], updates);
communityPackagesService.matchPackagesWithUpdates([pkgA, pkgB], updates);
expect(crossedPkgA.updateAvailable).toBe('0.2.0');
expect(crossedPkgB.updateAvailable).toBe('0.3.0');
@@ -229,7 +228,7 @@ describe('CommunityPackageService', () => {
};
const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] =
communityPackageService.matchPackagesWithUpdates([pkgA, pkgB], updates);
communityPackagesService.matchPackagesWithUpdates([pkgA, pkgB], updates);
expect(crossedPkgA.updateAvailable).toBeUndefined();
expect(crossedPkgB.updateAvailable).toBe('0.3.0');
@@ -239,12 +238,12 @@ describe('CommunityPackageService', () => {
describe('matchMissingPackages()', () => {
test('should not match failed packages that do not exist', () => {
const fakePkgs = mockPackagePair();
const notFoundPkgNames = `${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`;
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 = communityPackageService.matchMissingPackages(
fakePkgs,
notFoundPkgNames,
);
const matchedPackages = communityPackagesService.matchMissingPackages(fakePkgs);
expect(matchedPackages).toEqual(fakePkgs);
@@ -256,12 +255,15 @@ describe('CommunityPackageService', () => {
test('should match failed packages that should be present', () => {
const [pkgA, pkgB] = mockPackagePair();
const notFoundPkgNames = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${pkgA.packageName}@${pkgA.installedVersion}`;
setMissingPackages([
`${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0`,
`${pkgA.packageName}@${pkgA.installedVersion}`,
]);
const [matchedPkgA, matchedPkgB] = communityPackageService.matchMissingPackages(
[pkgA, pkgB],
notFoundPkgNames,
);
const [matchedPkgA, matchedPkgB] = communityPackagesService.matchMissingPackages([
pkgA,
pkgB,
]);
expect(matchedPkgA.failedLoading).toBe(true);
expect(matchedPkgB.failedLoading).toBeUndefined();
@@ -269,11 +271,14 @@ describe('CommunityPackageService', () => {
test('should match failed packages even if version is wrong', () => {
const [pkgA, pkgB] = mockPackagePair();
const notFoundPackageList = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${pkgA.packageName}@123.456.789`;
const [matchedPkgA, matchedPkgB] = communityPackageService.matchMissingPackages(
[pkgA, pkgB],
notFoundPackageList,
);
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();
@@ -282,7 +287,7 @@ describe('CommunityPackageService', () => {
describe('checkNpmPackageStatus()', () => {
test('should call axios.post', async () => {
await communityPackageService.checkNpmPackageStatus(mockPackageName());
await communityPackagesService.checkNpmPackageStatus(mockPackageName());
expect(axios.post).toHaveBeenCalled();
});
@@ -292,7 +297,7 @@ describe('CommunityPackageService', () => {
throw new Error('Something went wrong');
});
const result = await communityPackageService.checkNpmPackageStatus(mockPackageName());
const result = await communityPackagesService.checkNpmPackageStatus(mockPackageName());
expect(result.status).toBe(NPM_PACKAGE_STATUS_GOOD);
});
@@ -300,7 +305,7 @@ describe('CommunityPackageService', () => {
test('should warn if package is banned', async () => {
mocked(axios.post).mockResolvedValue({ data: { status: 'Banned', reason: 'Not good' } });
const result = (await communityPackageService.checkNpmPackageStatus(
const result = (await communityPackagesService.checkNpmPackageStatus(
mockPackageName(),
)) as CommunityPackages.PackageStatusCheck;
@@ -311,47 +316,50 @@ describe('CommunityPackageService', () => {
describe('hasPackageLoadedSuccessfully()', () => {
test('should return true when failed package list does not exist', () => {
config.set<string>('nodes.packagesMissing', undefined);
expect(communityPackageService.hasPackageLoaded('package')).toBe(true);
setMissingPackages([]);
expect(communityPackagesService.hasPackageLoaded('package')).toBe(true);
});
test('should return true when package is not in the list of missing packages', () => {
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.1.0');
expect(communityPackageService.hasPackageLoaded('packageC')).toBe(true);
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', () => {
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.1.0');
expect(communityPackageService.hasPackageLoaded('packageA')).toBe(false);
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', () => {
config.set<string>('nodes.packagesMissing', undefined);
setMissingPackages([]);
communityPackagesService.removePackageFromMissingList('packageA');
communityPackageService.removePackageFromMissingList('packageA');
expect(config.get('nodes.packagesMissing')).toBeUndefined();
expect(communityPackagesService.missingPackages).toBeEmptyArray();
});
test('should remove only correct package from list', () => {
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.2.0 packageC@0.2.0');
setMissingPackages(['packageA@0.1.0', 'packageB@0.2.0', 'packageC@0.2.0']);
communityPackageService.removePackageFromMissingList('packageB');
communityPackagesService.removePackageFromMissingList('packageB');
expect(config.get('nodes.packagesMissing')).toBe('packageA@0.1.0 packageC@0.2.0');
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';
config.set('nodes.packagesMissing', failedToLoadList);
communityPackageService.removePackageFromMissingList('packageC');
const failedToLoadList = ['packageA@0.1.0', 'packageB@0.2.0', 'packageB@0.2.0'];
setMissingPackages(failedToLoadList);
communityPackagesService.removePackageFromMissingList('packageC');
expect(config.get('nodes.packagesMissing')).toBe(failedToLoadList);
expect(communityPackagesService.missingPackages).toEqual(failedToLoadList);
});
});
const setMissingPackages = (missingPackages: string[]) => {
Object.assign(communityPackagesService, { missingPackages });
};
});