test(core): Move unit tests closer to testable components (no-changelog) (#10287)
This commit is contained in:
163
packages/cli/src/__tests__/ActiveExecutions.test.ts
Normal file
163
packages/cli/src/__tests__/ActiveExecutions.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { ActiveExecutions } from '@/ActiveExecutions';
|
||||
import PCancelable from 'p-cancelable';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { IExecuteResponsePromiseData, IRun } from 'n8n-workflow';
|
||||
import { createDeferredPromise } from 'n8n-workflow';
|
||||
import type { IWorkflowExecutionDataProcess } from '@/Interfaces';
|
||||
import type { ExecutionRepository } from '@db/repositories/execution.repository';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { ConcurrencyControlService } from '@/concurrency/concurrency-control.service';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
|
||||
const FAKE_EXECUTION_ID = '15';
|
||||
const FAKE_SECOND_EXECUTION_ID = '20';
|
||||
|
||||
const updateExistingExecution = jest.fn();
|
||||
const createNewExecution = jest.fn(async () => FAKE_EXECUTION_ID);
|
||||
|
||||
const executionRepository = mock<ExecutionRepository>({
|
||||
updateExistingExecution,
|
||||
createNewExecution,
|
||||
});
|
||||
|
||||
const concurrencyControl = mockInstance(ConcurrencyControlService, {
|
||||
// @ts-expect-error Private property
|
||||
isEnabled: false,
|
||||
});
|
||||
|
||||
describe('ActiveExecutions', () => {
|
||||
let activeExecutions: ActiveExecutions;
|
||||
|
||||
beforeEach(() => {
|
||||
activeExecutions = new ActiveExecutions(mock(), executionRepository, concurrencyControl);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('Should initialize activeExecutions with empty list', () => {
|
||||
expect(activeExecutions.getActiveExecutions().length).toBe(0);
|
||||
});
|
||||
|
||||
test('Should add execution to active execution list', async () => {
|
||||
const newExecution = mockExecutionData();
|
||||
const executionId = await activeExecutions.add(newExecution);
|
||||
|
||||
expect(executionId).toBe(FAKE_EXECUTION_ID);
|
||||
expect(activeExecutions.getActiveExecutions().length).toBe(1);
|
||||
expect(createNewExecution).toHaveBeenCalledTimes(1);
|
||||
expect(updateExistingExecution).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('Should update execution if add is called with execution ID', async () => {
|
||||
const newExecution = mockExecutionData();
|
||||
const executionId = await activeExecutions.add(newExecution, FAKE_SECOND_EXECUTION_ID);
|
||||
|
||||
expect(executionId).toBe(FAKE_SECOND_EXECUTION_ID);
|
||||
expect(activeExecutions.getActiveExecutions().length).toBe(1);
|
||||
expect(createNewExecution).toHaveBeenCalledTimes(0);
|
||||
expect(updateExistingExecution).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('Should fail attaching execution to invalid executionId', async () => {
|
||||
const deferredPromise = mockCancelablePromise();
|
||||
|
||||
expect(() => {
|
||||
activeExecutions.attachWorkflowExecution(FAKE_EXECUTION_ID, deferredPromise);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test('Should successfully attach execution to valid executionId', async () => {
|
||||
const newExecution = mockExecutionData();
|
||||
await activeExecutions.add(newExecution, FAKE_EXECUTION_ID);
|
||||
const deferredPromise = mockCancelablePromise();
|
||||
|
||||
expect(() =>
|
||||
activeExecutions.attachWorkflowExecution(FAKE_EXECUTION_ID, deferredPromise),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
test('Should attach and resolve response promise to existing execution', async () => {
|
||||
const newExecution = mockExecutionData();
|
||||
await activeExecutions.add(newExecution, FAKE_EXECUTION_ID);
|
||||
const deferredPromise = await mockDeferredPromise();
|
||||
activeExecutions.attachResponsePromise(FAKE_EXECUTION_ID, deferredPromise);
|
||||
const fakeResponse = { data: { resultData: { runData: {} } } };
|
||||
activeExecutions.resolveResponsePromise(FAKE_EXECUTION_ID, fakeResponse);
|
||||
|
||||
await expect(deferredPromise.promise()).resolves.toEqual(fakeResponse);
|
||||
});
|
||||
|
||||
test('Should remove an existing execution', async () => {
|
||||
const newExecution = mockExecutionData();
|
||||
const executionId = await activeExecutions.add(newExecution);
|
||||
activeExecutions.remove(executionId);
|
||||
|
||||
expect(activeExecutions.getActiveExecutions().length).toBe(0);
|
||||
});
|
||||
|
||||
test('Should resolve post execute promise on removal', async () => {
|
||||
const newExecution = mockExecutionData();
|
||||
const executionId = await activeExecutions.add(newExecution);
|
||||
const postExecutePromise = activeExecutions.getPostExecutePromise(executionId);
|
||||
// Force the above to be executed since we cannot await it
|
||||
await new Promise((res) => {
|
||||
setTimeout(res, 100);
|
||||
});
|
||||
const fakeOutput = mockFullRunData();
|
||||
activeExecutions.remove(executionId, fakeOutput);
|
||||
|
||||
await expect(postExecutePromise).resolves.toEqual(fakeOutput);
|
||||
});
|
||||
|
||||
test('Should throw error when trying to create a promise with invalid execution', async () => {
|
||||
await expect(activeExecutions.getPostExecutePromise(FAKE_EXECUTION_ID)).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('Should call function to cancel execution when asked to stop', async () => {
|
||||
const newExecution = mockExecutionData();
|
||||
const executionId = await activeExecutions.add(newExecution);
|
||||
const cancelExecution = jest.fn();
|
||||
const cancellablePromise = mockCancelablePromise();
|
||||
cancellablePromise.cancel = cancelExecution;
|
||||
activeExecutions.attachWorkflowExecution(executionId, cancellablePromise);
|
||||
void activeExecutions.stopExecution(executionId);
|
||||
|
||||
expect(cancelExecution).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
function mockExecutionData(): IWorkflowExecutionDataProcess {
|
||||
return {
|
||||
executionMode: 'manual',
|
||||
workflowData: {
|
||||
id: '123',
|
||||
name: 'Test workflow 1',
|
||||
active: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
nodes: [],
|
||||
connections: {},
|
||||
},
|
||||
userId: uuid(),
|
||||
};
|
||||
}
|
||||
|
||||
function mockFullRunData(): IRun {
|
||||
return {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {},
|
||||
},
|
||||
},
|
||||
mode: 'manual',
|
||||
startedAt: new Date(),
|
||||
status: 'new',
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
const mockCancelablePromise = () => new PCancelable<IRun>((resolve) => resolve());
|
||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
const mockDeferredPromise = () => createDeferredPromise<IExecuteResponsePromiseData>();
|
||||
40
packages/cli/src/__tests__/CredentialTypes.test.ts
Normal file
40
packages/cli/src/__tests__/CredentialTypes.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { CredentialTypes } from '@/CredentialTypes';
|
||||
import { Container } from 'typedi';
|
||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
|
||||
describe('CredentialTypes', () => {
|
||||
const mockNodesAndCredentials = mockInstance(LoadNodesAndCredentials, {
|
||||
loadedCredentials: {
|
||||
fakeFirstCredential: {
|
||||
type: {
|
||||
name: 'fakeFirstCredential',
|
||||
displayName: 'Fake First Credential',
|
||||
properties: [],
|
||||
},
|
||||
sourcePath: '',
|
||||
},
|
||||
fakeSecondCredential: {
|
||||
type: {
|
||||
name: 'fakeSecondCredential',
|
||||
displayName: 'Fake Second Credential',
|
||||
properties: [],
|
||||
},
|
||||
sourcePath: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const credentialTypes = Container.get(CredentialTypes);
|
||||
|
||||
test('Should throw error when calling invalid credential name', () => {
|
||||
expect(() => credentialTypes.getByName('fakeThirdCredential')).toThrowError();
|
||||
});
|
||||
|
||||
test('Should return correct credential type for valid name', () => {
|
||||
const mockedCredentialTypes = mockNodesAndCredentials.loadedCredentials;
|
||||
expect(credentialTypes.getByName('fakeFirstCredential')).toStrictEqual(
|
||||
mockedCredentialTypes.fakeFirstCredential.type,
|
||||
);
|
||||
});
|
||||
});
|
||||
296
packages/cli/src/__tests__/CredentialsHelper.test.ts
Normal file
296
packages/cli/src/__tests__/CredentialsHelper.test.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
import Container from 'typedi';
|
||||
import type {
|
||||
IAuthenticateGeneric,
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialType,
|
||||
IHttpRequestOptions,
|
||||
INode,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { deepCopy } from 'n8n-workflow';
|
||||
import { Workflow } from 'n8n-workflow';
|
||||
import { CredentialsHelper } from '@/CredentialsHelper';
|
||||
import { NodeTypes } from '@/NodeTypes';
|
||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||
import { CredentialsRepository } from '@db/repositories/credentials.repository';
|
||||
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
|
||||
describe('CredentialsHelper', () => {
|
||||
mockInstance(CredentialsRepository);
|
||||
mockInstance(SharedCredentialsRepository);
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const nodeTypes = mockInstance(NodeTypes);
|
||||
|
||||
describe('authenticate', () => {
|
||||
const tests: Array<{
|
||||
description: string;
|
||||
input: {
|
||||
credentials: ICredentialDataDecryptedObject;
|
||||
credentialType: ICredentialType;
|
||||
};
|
||||
output: IHttpRequestOptions;
|
||||
}> = [
|
||||
{
|
||||
description: 'basicAuth, default property names',
|
||||
input: {
|
||||
credentials: {
|
||||
user: 'user1',
|
||||
password: 'password1',
|
||||
},
|
||||
credentialType: new (class TestApi implements ICredentialType {
|
||||
name = 'testApi';
|
||||
|
||||
displayName = 'Test API';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'User',
|
||||
name: 'user',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Password',
|
||||
name: 'password',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
authenticate: IAuthenticateGeneric = {
|
||||
type: 'generic',
|
||||
properties: {
|
||||
auth: {
|
||||
username: '={{$credentials.user}}',
|
||||
password: '={{$credentials.password}}',
|
||||
},
|
||||
},
|
||||
};
|
||||
})(),
|
||||
},
|
||||
output: {
|
||||
url: '',
|
||||
headers: {},
|
||||
auth: { username: 'user1', password: 'password1' },
|
||||
qs: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'headerAuth',
|
||||
input: {
|
||||
credentials: {
|
||||
accessToken: 'test',
|
||||
},
|
||||
credentialType: new (class TestApi implements ICredentialType {
|
||||
name = 'testApi';
|
||||
|
||||
displayName = 'Test API';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Access Token',
|
||||
name: 'accessToken',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
authenticate: IAuthenticateGeneric = {
|
||||
type: 'generic',
|
||||
properties: {
|
||||
headers: {
|
||||
Authorization: '=Bearer {{$credentials.accessToken}}',
|
||||
},
|
||||
},
|
||||
};
|
||||
})(),
|
||||
},
|
||||
output: { url: '', headers: { Authorization: 'Bearer test' }, qs: {} },
|
||||
},
|
||||
{
|
||||
description: 'headerAuth, key and value expressions',
|
||||
input: {
|
||||
credentials: {
|
||||
accessToken: 'test',
|
||||
},
|
||||
credentialType: new (class TestApi implements ICredentialType {
|
||||
name = 'testApi';
|
||||
|
||||
displayName = 'Test API';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Access Token',
|
||||
name: 'accessToken',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
authenticate: IAuthenticateGeneric = {
|
||||
type: 'generic',
|
||||
properties: {
|
||||
headers: {
|
||||
'={{$credentials.accessToken}}': '=Bearer {{$credentials.accessToken}}',
|
||||
},
|
||||
},
|
||||
};
|
||||
})(),
|
||||
},
|
||||
output: { url: '', headers: { test: 'Bearer test' }, qs: {} },
|
||||
},
|
||||
{
|
||||
description: 'queryAuth',
|
||||
input: {
|
||||
credentials: {
|
||||
accessToken: 'test',
|
||||
},
|
||||
credentialType: new (class TestApi implements ICredentialType {
|
||||
name = 'testApi';
|
||||
|
||||
displayName = 'Test API';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Access Token',
|
||||
name: 'accessToken',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
authenticate = {
|
||||
type: 'generic',
|
||||
properties: {
|
||||
qs: {
|
||||
accessToken: '={{$credentials.accessToken}}',
|
||||
},
|
||||
},
|
||||
} as IAuthenticateGeneric;
|
||||
})(),
|
||||
},
|
||||
output: { url: '', headers: {}, qs: { accessToken: 'test' } },
|
||||
},
|
||||
{
|
||||
description: 'custom authentication',
|
||||
input: {
|
||||
credentials: {
|
||||
accessToken: 'test',
|
||||
user: 'testUser',
|
||||
},
|
||||
credentialType: new (class TestApi implements ICredentialType {
|
||||
name = 'testApi';
|
||||
|
||||
displayName = 'Test API';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'My Token',
|
||||
name: 'myToken',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
async authenticate(
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
requestOptions.headers!.Authorization = `Bearer ${credentials.accessToken}`;
|
||||
requestOptions.qs!.user = credentials.user;
|
||||
return requestOptions;
|
||||
}
|
||||
})(),
|
||||
},
|
||||
output: {
|
||||
url: '',
|
||||
headers: { Authorization: 'Bearer test' },
|
||||
qs: { user: 'testUser' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const node: INode = {
|
||||
id: 'uuid-1',
|
||||
parameters: {},
|
||||
name: 'test',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
};
|
||||
|
||||
const incomingRequestOptions = {
|
||||
url: '',
|
||||
headers: {},
|
||||
qs: {},
|
||||
};
|
||||
|
||||
const workflow = new Workflow({
|
||||
nodes: [node],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
for (const testData of tests) {
|
||||
test(testData.description, async () => {
|
||||
//@ts-expect-error `loadedCredentials` is a getter and we are replacing it here with a property
|
||||
mockNodesAndCredentials.loadedCredentials = {
|
||||
[testData.input.credentialType.name]: {
|
||||
type: testData.input.credentialType,
|
||||
sourcePath: '',
|
||||
},
|
||||
};
|
||||
|
||||
const credentialsHelper = Container.get(CredentialsHelper);
|
||||
|
||||
const result = await credentialsHelper.authenticate(
|
||||
testData.input.credentials,
|
||||
testData.input.credentialType.name,
|
||||
deepCopy(incomingRequestOptions),
|
||||
workflow,
|
||||
node,
|
||||
);
|
||||
|
||||
expect(result).toEqual(testData.output);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
273
packages/cli/src/__tests__/License.test.ts
Normal file
273
packages/cli/src/__tests__/License.test.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { LicenseManager } from '@n8n_io/license-sdk';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import config from '@/config';
|
||||
import { License } from '@/License';
|
||||
import { Logger } from '@/Logger';
|
||||
import { N8N_VERSION } from '@/constants';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
import { OrchestrationService } from '@/services/orchestration.service';
|
||||
|
||||
jest.mock('@n8n_io/license-sdk');
|
||||
|
||||
const MOCK_SERVER_URL = 'https://server.com/v1';
|
||||
const MOCK_RENEW_OFFSET = 259200;
|
||||
const MOCK_INSTANCE_ID = 'instance-id';
|
||||
const MOCK_ACTIVATION_KEY = 'activation-key';
|
||||
const MOCK_FEATURE_FLAG = 'feat:sharing';
|
||||
const MOCK_MAIN_PLAN_ID = '1b765dc4-d39d-4ffe-9885-c56dd67c4b26';
|
||||
|
||||
describe('License', () => {
|
||||
beforeAll(() => {
|
||||
config.set('license.serverUrl', MOCK_SERVER_URL);
|
||||
config.set('license.autoRenewEnabled', true);
|
||||
config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET);
|
||||
config.set('license.tenantId', 1);
|
||||
});
|
||||
|
||||
let license: License;
|
||||
const logger = mockInstance(Logger);
|
||||
const instanceSettings = mockInstance(InstanceSettings, { instanceId: MOCK_INSTANCE_ID });
|
||||
mockInstance(OrchestrationService);
|
||||
|
||||
beforeEach(async () => {
|
||||
license = new License(logger, instanceSettings, mock(), mock(), mock());
|
||||
await license.init();
|
||||
});
|
||||
|
||||
test('initializes license manager', async () => {
|
||||
expect(LicenseManager).toHaveBeenCalledWith({
|
||||
autoRenewEnabled: true,
|
||||
autoRenewOffset: MOCK_RENEW_OFFSET,
|
||||
offlineMode: false,
|
||||
renewOnInit: true,
|
||||
deviceFingerprint: expect.any(Function),
|
||||
productIdentifier: `n8n-${N8N_VERSION}`,
|
||||
logger,
|
||||
loadCertStr: expect.any(Function),
|
||||
saveCertStr: expect.any(Function),
|
||||
onFeatureChange: expect.any(Function),
|
||||
collectUsageMetrics: expect.any(Function),
|
||||
collectPassthroughData: expect.any(Function),
|
||||
server: MOCK_SERVER_URL,
|
||||
tenantId: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('initializes license manager for worker', async () => {
|
||||
license = new License(logger, instanceSettings, mock(), mock(), mock());
|
||||
await license.init('worker');
|
||||
expect(LicenseManager).toHaveBeenCalledWith({
|
||||
autoRenewEnabled: false,
|
||||
autoRenewOffset: MOCK_RENEW_OFFSET,
|
||||
offlineMode: true,
|
||||
renewOnInit: false,
|
||||
deviceFingerprint: expect.any(Function),
|
||||
productIdentifier: `n8n-${N8N_VERSION}`,
|
||||
logger,
|
||||
loadCertStr: expect.any(Function),
|
||||
saveCertStr: expect.any(Function),
|
||||
onFeatureChange: expect.any(Function),
|
||||
collectUsageMetrics: expect.any(Function),
|
||||
collectPassthroughData: expect.any(Function),
|
||||
server: MOCK_SERVER_URL,
|
||||
tenantId: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('attempts to activate license with provided key', async () => {
|
||||
await license.activate(MOCK_ACTIVATION_KEY);
|
||||
|
||||
expect(LicenseManager.prototype.activate).toHaveBeenCalledWith(MOCK_ACTIVATION_KEY);
|
||||
});
|
||||
|
||||
test('renews license', async () => {
|
||||
await license.renew();
|
||||
|
||||
expect(LicenseManager.prototype.renew).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('check if feature is enabled', () => {
|
||||
license.isFeatureEnabled(MOCK_FEATURE_FLAG);
|
||||
|
||||
expect(LicenseManager.prototype.hasFeatureEnabled).toHaveBeenCalledWith(MOCK_FEATURE_FLAG);
|
||||
});
|
||||
|
||||
test('check if sharing feature is enabled', () => {
|
||||
license.isFeatureEnabled(MOCK_FEATURE_FLAG);
|
||||
|
||||
expect(LicenseManager.prototype.hasFeatureEnabled).toHaveBeenCalledWith(MOCK_FEATURE_FLAG);
|
||||
});
|
||||
|
||||
test('check fetching entitlements', () => {
|
||||
license.getCurrentEntitlements();
|
||||
|
||||
expect(LicenseManager.prototype.getCurrentEntitlements).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('check fetching feature values', async () => {
|
||||
license.getFeatureValue(MOCK_FEATURE_FLAG);
|
||||
|
||||
expect(LicenseManager.prototype.getFeatureValue).toHaveBeenCalledWith(MOCK_FEATURE_FLAG);
|
||||
});
|
||||
|
||||
test('check management jwt', async () => {
|
||||
license.getManagementJwt();
|
||||
|
||||
expect(LicenseManager.prototype.getManagementJwt).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('getMainPlan() returns the right entitlement', async () => {
|
||||
// mock entitlements response
|
||||
License.prototype.getCurrentEntitlements = jest.fn().mockReturnValue([
|
||||
{
|
||||
id: '84a9c852-1349-478d-9ad1-b3f55510e477',
|
||||
productId: '670650f2-72d8-4397-898c-c249906e2cc2',
|
||||
productMetadata: {},
|
||||
features: {},
|
||||
featureOverrides: {},
|
||||
validFrom: new Date(),
|
||||
validTo: new Date(),
|
||||
},
|
||||
{
|
||||
id: MOCK_MAIN_PLAN_ID,
|
||||
productId: '670650f2-72d8-4397-898c-c249906e2cc2',
|
||||
productMetadata: {
|
||||
terms: {
|
||||
isMainPlan: true,
|
||||
},
|
||||
},
|
||||
features: {},
|
||||
featureOverrides: {},
|
||||
validFrom: new Date(),
|
||||
validTo: new Date(),
|
||||
},
|
||||
]);
|
||||
jest.fn(license.getMainPlan).mockReset();
|
||||
|
||||
const mainPlan = license.getMainPlan();
|
||||
expect(mainPlan?.id).toBe(MOCK_MAIN_PLAN_ID);
|
||||
});
|
||||
|
||||
test('getMainPlan() returns undefined if there is no main plan', async () => {
|
||||
// mock entitlements response
|
||||
License.prototype.getCurrentEntitlements = jest.fn().mockReturnValue([
|
||||
{
|
||||
id: '84a9c852-1349-478d-9ad1-b3f55510e477',
|
||||
productId: '670650f2-72d8-4397-898c-c249906e2cc2',
|
||||
productMetadata: {}, // has no `productMetadata.terms.isMainPlan`!
|
||||
features: {},
|
||||
featureOverrides: {},
|
||||
validFrom: new Date(),
|
||||
validTo: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'c1aae471-c24e-4874-ad88-b97107de486c',
|
||||
productId: '670650f2-72d8-4397-898c-c249906e2cc2',
|
||||
productMetadata: {}, // has no `productMetadata.terms.isMainPlan`!
|
||||
features: {},
|
||||
featureOverrides: {},
|
||||
validFrom: new Date(),
|
||||
validTo: new Date(),
|
||||
},
|
||||
]);
|
||||
jest.fn(license.getMainPlan).mockReset();
|
||||
|
||||
const mainPlan = license.getMainPlan();
|
||||
expect(mainPlan).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('License', () => {
|
||||
beforeEach(() => {
|
||||
config.load(config.default);
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
describe('in single-main setup', () => {
|
||||
describe('with `license.autoRenewEnabled` enabled', () => {
|
||||
it('should enable renewal', async () => {
|
||||
config.set('multiMainSetup.enabled', false);
|
||||
|
||||
await new License(mock(), mock(), mock(), mock(), mock()).init();
|
||||
|
||||
expect(LicenseManager).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ autoRenewEnabled: true, renewOnInit: true }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with `license.autoRenewEnabled` disabled', () => {
|
||||
it('should disable renewal', async () => {
|
||||
config.set('license.autoRenewEnabled', false);
|
||||
|
||||
await new License(mock(), mock(), mock(), mock(), mock()).init();
|
||||
|
||||
expect(LicenseManager).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ autoRenewEnabled: false, renewOnInit: false }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('in multi-main setup', () => {
|
||||
describe('with `license.autoRenewEnabled` disabled', () => {
|
||||
test.each(['unset', 'leader', 'follower'])(
|
||||
'if %s status, should disable removal',
|
||||
async (status) => {
|
||||
config.set('multiMainSetup.enabled', true);
|
||||
config.set('multiMainSetup.instanceType', status);
|
||||
config.set('license.autoRenewEnabled', false);
|
||||
|
||||
await new License(mock(), mock(), mock(), mock(), mock()).init();
|
||||
|
||||
expect(LicenseManager).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ autoRenewEnabled: false, renewOnInit: false }),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('with `license.autoRenewEnabled` enabled', () => {
|
||||
test.each(['unset', 'follower'])('if %s status, should disable removal', async (status) => {
|
||||
config.set('multiMainSetup.enabled', true);
|
||||
config.set('multiMainSetup.instanceType', status);
|
||||
config.set('license.autoRenewEnabled', false);
|
||||
|
||||
await new License(mock(), mock(), mock(), mock(), mock()).init();
|
||||
|
||||
expect(LicenseManager).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ autoRenewEnabled: false, renewOnInit: false }),
|
||||
);
|
||||
});
|
||||
|
||||
it('if leader status, should enable renewal', async () => {
|
||||
config.set('multiMainSetup.enabled', true);
|
||||
config.set('multiMainSetup.instanceType', 'leader');
|
||||
|
||||
await new License(mock(), mock(), mock(), mock(), mock()).init();
|
||||
|
||||
expect(LicenseManager).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ autoRenewEnabled: true, renewOnInit: true }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reinit', () => {
|
||||
it('should reinitialize license manager', async () => {
|
||||
const license = new License(mock(), mock(), mock(), mock(), mock());
|
||||
await license.init();
|
||||
|
||||
const initSpy = jest.spyOn(license, 'init');
|
||||
|
||||
await license.reinit();
|
||||
|
||||
expect(initSpy).toHaveBeenCalledWith('main', true);
|
||||
|
||||
expect(LicenseManager.prototype.reset).toHaveBeenCalled();
|
||||
expect(LicenseManager.prototype.initialize).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
134
packages/cli/src/__tests__/TestWebhooks.test.ts
Normal file
134
packages/cli/src/__tests__/TestWebhooks.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { TestWebhooks } from '@/TestWebhooks';
|
||||
import { WebhookNotFoundError } from '@/errors/response-errors/webhook-not-found.error';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { generateNanoId } from '@/databases/utils/generators';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import * as WebhookHelpers from '@/WebhookHelpers';
|
||||
import type * as express from 'express';
|
||||
|
||||
import type { IWorkflowDb, WebhookRequest } from '@/Interfaces';
|
||||
import type { IWebhookData, IWorkflowExecuteAdditionalData, Workflow } from 'n8n-workflow';
|
||||
import type {
|
||||
TestWebhookRegistrationsService,
|
||||
TestWebhookRegistration,
|
||||
} from '@/services/test-webhook-registrations.service';
|
||||
|
||||
import * as AdditionalData from '@/WorkflowExecuteAdditionalData';
|
||||
|
||||
jest.mock('@/WorkflowExecuteAdditionalData');
|
||||
|
||||
const mockedAdditionalData = AdditionalData as jest.Mocked<typeof AdditionalData>;
|
||||
|
||||
const workflowEntity = mock<IWorkflowDb>({ id: generateNanoId(), nodes: [] });
|
||||
|
||||
const httpMethod = 'GET';
|
||||
const path = uuid();
|
||||
const userId = '04ab4baf-85df-478f-917b-d303934a97de';
|
||||
|
||||
const webhook = mock<IWebhookData>({
|
||||
httpMethod,
|
||||
path,
|
||||
workflowId: workflowEntity.id,
|
||||
userId,
|
||||
});
|
||||
|
||||
const registrations = mock<TestWebhookRegistrationsService>();
|
||||
|
||||
let testWebhooks: TestWebhooks;
|
||||
|
||||
describe('TestWebhooks', () => {
|
||||
beforeAll(() => {
|
||||
testWebhooks = new TestWebhooks(mock(), mock(), registrations, mock());
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
describe('needsWebhook()', () => {
|
||||
const args: Parameters<typeof testWebhooks.needsWebhook> = [
|
||||
userId,
|
||||
workflowEntity,
|
||||
mock<IWorkflowExecuteAdditionalData>(),
|
||||
];
|
||||
|
||||
test('if webhook is needed, should register then create webhook and return true', async () => {
|
||||
const workflow = mock<Workflow>();
|
||||
|
||||
jest.spyOn(testWebhooks, 'toWorkflow').mockReturnValueOnce(workflow);
|
||||
jest.spyOn(WebhookHelpers, 'getWorkflowWebhooks').mockReturnValue([webhook]);
|
||||
|
||||
const needsWebhook = await testWebhooks.needsWebhook(...args);
|
||||
|
||||
const [registerOrder] = registrations.register.mock.invocationCallOrder;
|
||||
const [createOrder] = workflow.createWebhookIfNotExists.mock.invocationCallOrder;
|
||||
|
||||
expect(registerOrder).toBeLessThan(createOrder);
|
||||
expect(needsWebhook).toBe(true);
|
||||
});
|
||||
|
||||
test('if webhook activation fails, should deactivate workflow webhooks', async () => {
|
||||
const msg = 'Failed to add webhook to active webhooks';
|
||||
|
||||
jest.spyOn(WebhookHelpers, 'getWorkflowWebhooks').mockReturnValue([webhook]);
|
||||
jest.spyOn(registrations, 'register').mockRejectedValueOnce(new Error(msg));
|
||||
registrations.getAllRegistrations.mockResolvedValue([]);
|
||||
|
||||
const needsWebhook = testWebhooks.needsWebhook(...args);
|
||||
|
||||
await expect(needsWebhook).rejects.toThrowError(msg);
|
||||
});
|
||||
|
||||
test('if no webhook is found to start workflow, should return false', async () => {
|
||||
webhook.webhookDescription.restartWebhook = true;
|
||||
jest.spyOn(WebhookHelpers, 'getWorkflowWebhooks').mockReturnValue([webhook]);
|
||||
|
||||
const result = await testWebhooks.needsWebhook(...args);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeWebhook()', () => {
|
||||
test('if webhook is not registered, should throw', async () => {
|
||||
jest.spyOn(testWebhooks, 'getActiveWebhook').mockResolvedValue(webhook);
|
||||
jest.spyOn(testWebhooks, 'getWebhookMethods').mockResolvedValue([]);
|
||||
|
||||
const promise = testWebhooks.executeWebhook(
|
||||
mock<WebhookRequest>({ params: { path } }),
|
||||
mock(),
|
||||
);
|
||||
|
||||
await expect(promise).rejects.toThrowError(WebhookNotFoundError);
|
||||
});
|
||||
|
||||
test('if webhook is registered but missing from workflow, should throw', async () => {
|
||||
jest.spyOn(testWebhooks, 'getActiveWebhook').mockResolvedValue(webhook);
|
||||
jest.spyOn(testWebhooks, 'getWebhookMethods').mockResolvedValue([]);
|
||||
|
||||
const registration = mock<TestWebhookRegistration>({
|
||||
pushRef: 'some-session-id',
|
||||
workflowEntity,
|
||||
});
|
||||
|
||||
await registrations.register(registration);
|
||||
|
||||
const promise = testWebhooks.executeWebhook(
|
||||
mock<WebhookRequest>({ params: { path } }),
|
||||
mock<express.Response>(),
|
||||
);
|
||||
|
||||
await expect(promise).rejects.toThrowError(NotFoundError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deactivateWebhooks()', () => {
|
||||
test('should add additional data to workflow', async () => {
|
||||
registrations.getAllRegistrations.mockResolvedValue([{ workflowEntity, webhook }]);
|
||||
|
||||
const workflow = testWebhooks.toWorkflow(workflowEntity);
|
||||
|
||||
await testWebhooks.deactivateWebhooks(workflow);
|
||||
|
||||
expect(mockedAdditionalData.getBase).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
});
|
||||
});
|
||||
142
packages/cli/src/__tests__/WaitTracker.test.ts
Normal file
142
packages/cli/src/__tests__/WaitTracker.test.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { WaitTracker } from '@/WaitTracker';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||
import type { IExecutionResponse } from '@/Interfaces';
|
||||
import { OrchestrationService } from '@/services/orchestration.service';
|
||||
import type { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('WaitTracker', () => {
|
||||
const executionRepository = mock<ExecutionRepository>();
|
||||
const multiMainSetup = mock<MultiMainSetup>();
|
||||
const orchestrationService = new OrchestrationService(mock(), mock(), mock(), multiMainSetup);
|
||||
|
||||
const execution = mock<IExecutionResponse>({
|
||||
id: '123',
|
||||
waitTill: new Date(Date.now() + 1000),
|
||||
});
|
||||
|
||||
let waitTracker: WaitTracker;
|
||||
beforeEach(() => {
|
||||
waitTracker = new WaitTracker(
|
||||
mock(),
|
||||
executionRepository,
|
||||
mock(),
|
||||
mock(),
|
||||
orchestrationService,
|
||||
);
|
||||
multiMainSetup.on.mockReturnThis();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('init()', () => {
|
||||
it('should query DB for waiting executions if leader', async () => {
|
||||
jest.spyOn(orchestrationService, 'isLeader', 'get').mockReturnValue(true);
|
||||
executionRepository.getWaitingExecutions.mockResolvedValue([execution]);
|
||||
|
||||
waitTracker.init();
|
||||
|
||||
expect(executionRepository.getWaitingExecutions).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('if follower, should do nothing', () => {
|
||||
executionRepository.getWaitingExecutions.mockResolvedValue([]);
|
||||
|
||||
waitTracker.init();
|
||||
|
||||
expect(executionRepository.findSingleExecution).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('if no executions to start, should do nothing', () => {
|
||||
executionRepository.getWaitingExecutions.mockResolvedValue([]);
|
||||
|
||||
waitTracker.init();
|
||||
|
||||
expect(executionRepository.findSingleExecution).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('if execution to start', () => {
|
||||
it('if not enough time passed, should not start execution', async () => {
|
||||
executionRepository.getWaitingExecutions.mockResolvedValue([execution]);
|
||||
waitTracker.init();
|
||||
|
||||
executionRepository.getWaitingExecutions.mockResolvedValue([execution]);
|
||||
await waitTracker.getWaitingExecutions();
|
||||
|
||||
const startExecutionSpy = jest.spyOn(waitTracker, 'startExecution');
|
||||
|
||||
jest.advanceTimersByTime(100);
|
||||
|
||||
expect(startExecutionSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('if enough time passed, should start execution', async () => {
|
||||
executionRepository.getWaitingExecutions.mockResolvedValue([]);
|
||||
waitTracker.init();
|
||||
|
||||
executionRepository.getWaitingExecutions.mockResolvedValue([execution]);
|
||||
await waitTracker.getWaitingExecutions();
|
||||
|
||||
const startExecutionSpy = jest.spyOn(waitTracker, 'startExecution');
|
||||
|
||||
jest.advanceTimersByTime(2_000);
|
||||
|
||||
expect(startExecutionSpy).toHaveBeenCalledWith(execution.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('startExecution()', () => {
|
||||
it('should query for execution to start', async () => {
|
||||
executionRepository.getWaitingExecutions.mockResolvedValue([]);
|
||||
waitTracker.init();
|
||||
|
||||
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
||||
waitTracker.startExecution(execution.id);
|
||||
jest.advanceTimersByTime(5);
|
||||
|
||||
expect(executionRepository.findSingleExecution).toHaveBeenCalledWith(execution.id, {
|
||||
includeData: true,
|
||||
unflattenData: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('single-main setup', () => {
|
||||
it('should start tracking', () => {
|
||||
executionRepository.getWaitingExecutions.mockResolvedValue([]);
|
||||
|
||||
waitTracker.init();
|
||||
|
||||
expect(executionRepository.getWaitingExecutions).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi-main setup', () => {
|
||||
it('should start tracking if leader', () => {
|
||||
jest.spyOn(orchestrationService, 'isLeader', 'get').mockReturnValue(true);
|
||||
jest.spyOn(orchestrationService, 'isSingleMainSetup', 'get').mockReturnValue(false);
|
||||
|
||||
executionRepository.getWaitingExecutions.mockResolvedValue([]);
|
||||
|
||||
waitTracker.init();
|
||||
|
||||
expect(executionRepository.getWaitingExecutions).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not start tracking if follower', () => {
|
||||
jest.spyOn(orchestrationService, 'isLeader', 'get').mockReturnValue(false);
|
||||
jest.spyOn(orchestrationService, 'isSingleMainSetup', 'get').mockReturnValue(false);
|
||||
|
||||
executionRepository.getWaitingExecutions.mockResolvedValue([]);
|
||||
|
||||
waitTracker.init();
|
||||
|
||||
expect(executionRepository.getWaitingExecutions).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
142
packages/cli/src/__tests__/WebhookHelpers.test.ts
Normal file
142
packages/cli/src/__tests__/WebhookHelpers.test.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { type Response } from 'express';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { randomString } from 'n8n-workflow';
|
||||
import type { IHttpRequestMethods } from 'n8n-workflow';
|
||||
|
||||
import type { IWebhookManager, WebhookCORSRequest, WebhookRequest } from '@/Interfaces';
|
||||
import { webhookRequestHandler } from '@/WebhookHelpers';
|
||||
|
||||
describe('WebhookHelpers', () => {
|
||||
describe('webhookRequestHandler', () => {
|
||||
const webhookManager = mock<Required<IWebhookManager>>();
|
||||
const handler = webhookRequestHandler(webhookManager);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should throw for unsupported methods', async () => {
|
||||
const req = mock<WebhookRequest | WebhookCORSRequest>({
|
||||
method: 'CONNECT' as IHttpRequestMethods,
|
||||
});
|
||||
const res = mock<Response>();
|
||||
res.status.mockReturnValue(res);
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
code: 0,
|
||||
message: 'The method CONNECT is not supported.',
|
||||
});
|
||||
});
|
||||
|
||||
describe('preflight requests', () => {
|
||||
it('should handle missing header for requested method', async () => {
|
||||
const req = mock<WebhookRequest | WebhookCORSRequest>({
|
||||
method: 'OPTIONS',
|
||||
headers: {
|
||||
origin: 'https://example.com',
|
||||
'access-control-request-method': undefined,
|
||||
},
|
||||
params: { path: 'test' },
|
||||
});
|
||||
const res = mock<Response>();
|
||||
res.status.mockReturnValue(res);
|
||||
|
||||
webhookManager.getWebhookMethods.mockResolvedValue(['GET', 'PATCH']);
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(204);
|
||||
expect(res.header).toHaveBeenCalledWith(
|
||||
'Access-Control-Allow-Methods',
|
||||
'OPTIONS, GET, PATCH',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle default origin and max-age', async () => {
|
||||
const req = mock<WebhookRequest | WebhookCORSRequest>({
|
||||
method: 'OPTIONS',
|
||||
headers: {
|
||||
origin: 'https://example.com',
|
||||
'access-control-request-method': 'GET',
|
||||
},
|
||||
params: { path: 'test' },
|
||||
});
|
||||
const res = mock<Response>();
|
||||
res.status.mockReturnValue(res);
|
||||
|
||||
webhookManager.getWebhookMethods.mockResolvedValue(['GET', 'PATCH']);
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(204);
|
||||
expect(res.header).toHaveBeenCalledWith(
|
||||
'Access-Control-Allow-Methods',
|
||||
'OPTIONS, GET, PATCH',
|
||||
);
|
||||
expect(res.header).toHaveBeenCalledWith(
|
||||
'Access-Control-Allow-Origin',
|
||||
'https://example.com',
|
||||
);
|
||||
expect(res.header).toHaveBeenCalledWith('Access-Control-Max-Age', '300');
|
||||
});
|
||||
|
||||
it('should handle wildcard origin', async () => {
|
||||
const randomOrigin = randomString(10);
|
||||
const req = mock<WebhookRequest | WebhookCORSRequest>({
|
||||
method: 'OPTIONS',
|
||||
headers: {
|
||||
origin: randomOrigin,
|
||||
'access-control-request-method': 'GET',
|
||||
},
|
||||
params: { path: 'test' },
|
||||
});
|
||||
const res = mock<Response>();
|
||||
res.status.mockReturnValue(res);
|
||||
|
||||
webhookManager.getWebhookMethods.mockResolvedValue(['GET', 'PATCH']);
|
||||
webhookManager.findAccessControlOptions.mockResolvedValue({
|
||||
allowedOrigins: '*',
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(204);
|
||||
expect(res.header).toHaveBeenCalledWith(
|
||||
'Access-Control-Allow-Methods',
|
||||
'OPTIONS, GET, PATCH',
|
||||
);
|
||||
expect(res.header).toHaveBeenCalledWith('Access-Control-Allow-Origin', randomOrigin);
|
||||
});
|
||||
|
||||
it('should handle custom origin', async () => {
|
||||
const req = mock<WebhookRequest | WebhookCORSRequest>({
|
||||
method: 'OPTIONS',
|
||||
headers: {
|
||||
origin: 'https://example.com',
|
||||
'access-control-request-method': 'GET',
|
||||
},
|
||||
params: { path: 'test' },
|
||||
});
|
||||
const res = mock<Response>();
|
||||
res.status.mockReturnValue(res);
|
||||
|
||||
webhookManager.getWebhookMethods.mockResolvedValue(['GET', 'PATCH']);
|
||||
webhookManager.findAccessControlOptions.mockResolvedValue({
|
||||
allowedOrigins: 'https://test.com',
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(204);
|
||||
expect(res.header).toHaveBeenCalledWith(
|
||||
'Access-Control-Allow-Methods',
|
||||
'OPTIONS, GET, PATCH',
|
||||
);
|
||||
expect(res.header).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'https://test.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { VariablesService } from '@/environments/variables/variables.service.ee';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
|
||||
import { getBase } from '@/WorkflowExecuteAdditionalData';
|
||||
import Container from 'typedi';
|
||||
import { CredentialsHelper } from '@/CredentialsHelper';
|
||||
import { SecretsHelper } from '@/SecretsHelpers';
|
||||
|
||||
describe('WorkflowExecuteAdditionalData', () => {
|
||||
const messageEventBus = mockInstance(MessageEventBus);
|
||||
const variablesService = mockInstance(VariablesService);
|
||||
variablesService.getAllCached.mockResolvedValue([]);
|
||||
const credentialsHelper = mockInstance(CredentialsHelper);
|
||||
const secretsHelper = mockInstance(SecretsHelper);
|
||||
Container.set(MessageEventBus, messageEventBus);
|
||||
Container.set(VariablesService, variablesService);
|
||||
Container.set(CredentialsHelper, credentialsHelper);
|
||||
Container.set(SecretsHelper, secretsHelper);
|
||||
|
||||
test('logAiEvent should call MessageEventBus', async () => {
|
||||
const additionalData = await getBase('user-id');
|
||||
|
||||
const eventName = 'n8n.ai.memory.get.messages';
|
||||
const payload = {
|
||||
msg: 'test message',
|
||||
executionId: '123',
|
||||
nodeName: 'n8n-memory',
|
||||
workflowId: 'workflow-id',
|
||||
workflowName: 'workflow-name',
|
||||
nodeType: 'n8n-memory',
|
||||
};
|
||||
|
||||
await additionalData.logAiEvent(eventName, payload);
|
||||
|
||||
expect(messageEventBus.sendAiNodeEvent).toHaveBeenCalledTimes(1);
|
||||
expect(messageEventBus.sendAiNodeEvent).toHaveBeenCalledWith({
|
||||
eventName,
|
||||
payload,
|
||||
});
|
||||
});
|
||||
});
|
||||
46
packages/cli/src/__tests__/WorkflowHelpers.test.ts
Normal file
46
packages/cli/src/__tests__/WorkflowHelpers.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { type Workflow } from 'n8n-workflow';
|
||||
import { getExecutionStartNode } from '@/WorkflowHelpers';
|
||||
import type { IWorkflowExecutionDataProcess } from '@/Interfaces';
|
||||
|
||||
describe('WorkflowHelpers', () => {
|
||||
describe('getExecutionStartNode', () => {
|
||||
it('Should return undefined', () => {
|
||||
const data = {
|
||||
pinData: {},
|
||||
startNodes: [],
|
||||
} as unknown as IWorkflowExecutionDataProcess;
|
||||
const workflow = {
|
||||
getNode(nodeName: string) {
|
||||
return {
|
||||
name: nodeName,
|
||||
};
|
||||
},
|
||||
} as unknown as Workflow;
|
||||
const executionStartNode = getExecutionStartNode(data, workflow);
|
||||
expect(executionStartNode).toBeUndefined();
|
||||
});
|
||||
it('Should return startNode', () => {
|
||||
const data = {
|
||||
pinData: {
|
||||
node1: {},
|
||||
node2: {},
|
||||
},
|
||||
startNodes: [{ name: 'node2' }],
|
||||
} as unknown as IWorkflowExecutionDataProcess;
|
||||
const workflow = {
|
||||
getNode(nodeName: string) {
|
||||
if (nodeName === 'node2') {
|
||||
return {
|
||||
name: 'node2',
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
} as unknown as Workflow;
|
||||
const executionStartNode = getExecutionStartNode(data, workflow);
|
||||
expect(executionStartNode).toEqual({
|
||||
name: 'node2',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
84
packages/cli/src/__tests__/WorkflowRunner.test.ts
Normal file
84
packages/cli/src/__tests__/WorkflowRunner.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import Container from 'typedi';
|
||||
import { WorkflowHooks, type ExecutionError, type IWorkflowExecuteHooks } from 'n8n-workflow';
|
||||
import type { User } from '@db/entities/User';
|
||||
import { WorkflowRunner } from '@/WorkflowRunner';
|
||||
import config from '@/config';
|
||||
|
||||
import * as testDb from '@test-integration/testDb';
|
||||
import { setupTestServer } from '@test-integration/utils';
|
||||
import { createUser } from '@test-integration/db/users';
|
||||
import { createWorkflow } from '@test-integration/db/workflows';
|
||||
import { createExecution } from '@test-integration/db/executions';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
import { Telemetry } from '@/telemetry';
|
||||
|
||||
let owner: User;
|
||||
let runner: WorkflowRunner;
|
||||
let hookFunctions: IWorkflowExecuteHooks;
|
||||
setupTestServer({ endpointGroups: [] });
|
||||
|
||||
mockInstance(Telemetry);
|
||||
|
||||
class Watchers {
|
||||
workflowExecuteAfter = jest.fn();
|
||||
}
|
||||
const watchers = new Watchers();
|
||||
const watchedWorkflowExecuteAfter = jest.spyOn(watchers, 'workflowExecuteAfter');
|
||||
|
||||
beforeAll(async () => {
|
||||
owner = await createUser({ role: 'global:owner' });
|
||||
|
||||
runner = Container.get(WorkflowRunner);
|
||||
|
||||
hookFunctions = {
|
||||
workflowExecuteAfter: [watchers.workflowExecuteAfter],
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['Workflow', 'SharedWorkflow']);
|
||||
});
|
||||
|
||||
test('processError should return early in Bull stalled edge case', async () => {
|
||||
const workflow = await createWorkflow({}, owner);
|
||||
const execution = await createExecution(
|
||||
{
|
||||
status: 'success',
|
||||
finished: true,
|
||||
},
|
||||
workflow,
|
||||
);
|
||||
config.set('executions.mode', 'queue');
|
||||
await runner.processError(
|
||||
new Error('test') as ExecutionError,
|
||||
new Date(),
|
||||
'webhook',
|
||||
execution.id,
|
||||
new WorkflowHooks(hookFunctions, 'webhook', execution.id, workflow),
|
||||
);
|
||||
expect(watchedWorkflowExecuteAfter).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('processError should process error', async () => {
|
||||
const workflow = await createWorkflow({}, owner);
|
||||
const execution = await createExecution(
|
||||
{
|
||||
status: 'success',
|
||||
finished: true,
|
||||
},
|
||||
workflow,
|
||||
);
|
||||
config.set('executions.mode', 'regular');
|
||||
await runner.processError(
|
||||
new Error('test') as ExecutionError,
|
||||
new Date(),
|
||||
'webhook',
|
||||
execution.id,
|
||||
new WorkflowHooks(hookFunctions, 'webhook', execution.id, workflow),
|
||||
);
|
||||
expect(watchedWorkflowExecuteAfter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
Reference in New Issue
Block a user