Files
n8n_Demo/n8n-n8n-1.109.2/packages/cli/src/__tests__/external-hooks.test.ts
2025-09-08 04:48:28 +08:00

129 lines
3.7 KiB
TypeScript
Executable File

import type { Logger } from '@n8n/backend-common';
import type { GlobalConfig } from '@n8n/config';
import type {
WorkflowRepository,
CredentialsRepository,
SettingsRepository,
UserRepository,
} from '@n8n/db';
import { mock } from 'jest-mock-extended';
import type { ErrorReporter } from 'n8n-core';
import type { IWorkflowBase } from 'n8n-workflow';
import { UnexpectedError } from 'n8n-workflow';
import { ExternalHooks } from '@/external-hooks';
describe('ExternalHooks', () => {
const logger = mock<Logger>();
const errorReporter = mock<ErrorReporter>();
const globalConfig = mock<GlobalConfig>();
const userRepository = mock<UserRepository>();
const settingsRepository = mock<SettingsRepository>();
const credentialsRepository = mock<CredentialsRepository>();
const workflowRepository = mock<WorkflowRepository>();
const workflowData = mock<IWorkflowBase>({ id: '123', name: 'Test Workflow' });
const hookFn = jest.fn();
let externalHooks: ExternalHooks;
beforeEach(() => {
jest.resetAllMocks();
globalConfig.externalHooks.files = [];
externalHooks = new ExternalHooks(
logger,
errorReporter,
globalConfig,
userRepository,
settingsRepository,
credentialsRepository,
workflowRepository,
);
});
describe('init()', () => {
it('should not load hooks if no external hook files are configured', async () => {
// @ts-expect-error private method
const loadHooksSpy = jest.spyOn(externalHooks, 'loadHooks');
await externalHooks.init();
expect(loadHooksSpy).not.toHaveBeenCalled();
});
it('should throw an error if hook file cannot be loaded', async () => {
globalConfig.externalHooks.files = ['/path/to/non-existent-hook.js'];
jest.mock(
'/path/to/non-existent-hook.js',
() => {
throw new Error('File not found');
},
{ virtual: true },
);
await expect(externalHooks.init()).rejects.toThrow(UnexpectedError);
});
it('should successfully load hooks from valid hook file', async () => {
const mockHookFile = {
workflow: {
create: [hookFn],
},
};
globalConfig.externalHooks.files = ['/path/to/valid-hook.js'];
jest.mock('/path/to/valid-hook.js', () => mockHookFile, { virtual: true });
await externalHooks.init();
expect(externalHooks['registered']['workflow.create']).toHaveLength(1);
await externalHooks.run('workflow.create', [workflowData]);
expect(hookFn).toHaveBeenCalledTimes(1);
expect(hookFn).toHaveBeenCalledWith(workflowData);
});
});
describe('run()', () => {
it('should not throw if no hooks are registered', async () => {
await externalHooks.run('n8n.stop');
});
it('should execute registered hooks', async () => {
externalHooks['registered']['workflow.create'] = [hookFn];
await externalHooks.run('workflow.create', [workflowData]);
expect(hookFn).toHaveBeenCalledTimes(1);
const hookInvocationContext = hookFn.mock.instances[0];
expect(hookInvocationContext).toHaveProperty('dbCollections');
expect(hookInvocationContext.dbCollections).toEqual({
User: userRepository,
Settings: settingsRepository,
Credentials: credentialsRepository,
Workflow: workflowRepository,
});
});
it('should report error if hook execution fails', async () => {
const error = new Error('Hook failed');
hookFn.mockRejectedValueOnce(error);
externalHooks['registered']['workflow.create'] = [hookFn];
await expect(externalHooks.run('workflow.create', [workflowData])).rejects.toThrow(error);
expect(errorReporter.error).toHaveBeenCalledWith(
expect.objectContaining({
message: 'External hook "workflow.create" failed',
cause: error,
}),
{ level: 'fatal' },
);
expect(logger.error).toHaveBeenCalledWith(
'There was a problem running hook "workflow.create"',
);
});
});
});