Files
n8n-enterprise-unlocked/packages/cli/src/webhooks/__tests__/webhook.service.test.ts
कारतोफ्फेलस्क्रिप्ट™ 5a055ed526 refactor(core): Move execution engine code out of n8n-workflow (no-changelog) (#12147)
2024-12-12 13:54:44 +01:00

364 lines
11 KiB
TypeScript

import { mock } from 'jest-mock-extended';
import type { INode, INodeType, IWebhookData, IWorkflowExecuteAdditionalData } from 'n8n-workflow';
import { Workflow } from 'n8n-workflow';
import { v4 as uuid } from 'uuid';
import config from '@/config';
import { WebhookEntity } from '@/databases/entities/webhook-entity';
import type { WebhookRepository } from '@/databases/repositories/webhook.repository';
import type { NodeTypes } from '@/node-types';
import type { CacheService } from '@/services/cache/cache.service';
import { WebhookService } from '@/webhooks/webhook.service';
const createWebhook = (method: string, path: string, webhookId?: string, pathSegments?: number) =>
Object.assign(new WebhookEntity(), {
method,
webhookPath: path,
webhookId,
pathSegments,
}) as WebhookEntity;
describe('WebhookService', () => {
const webhookRepository = mock<WebhookRepository>();
const cacheService = mock<CacheService>();
const nodeTypes = mock<NodeTypes>();
const webhookService = new WebhookService(mock(), webhookRepository, cacheService, nodeTypes);
const additionalData = mock<IWorkflowExecuteAdditionalData>();
beforeEach(() => {
config.load(config.default);
jest.clearAllMocks();
});
[true, false].forEach((isCacheEnabled) => {
const tag = '[' + ['cache', isCacheEnabled ? 'enabled' : 'disabled'].join(' ') + ']';
describe(`findWebhook() - static case ${tag}`, () => {
test('should return the webhook if found', async () => {
const method = 'GET';
const path = 'user/profile';
const mockWebhook = createWebhook(method, path);
webhookRepository.findOneBy.mockResolvedValue(mockWebhook);
const returnedWebhook = await webhookService.findWebhook(method, path);
expect(returnedWebhook).toBe(mockWebhook);
});
test('should return null if not found', async () => {
webhookRepository.findOneBy.mockResolvedValue(null); // static
webhookRepository.findBy.mockResolvedValue([]);
const returnValue = await webhookService.findWebhook('GET', 'user/profile');
expect(returnValue).toBeNull();
});
});
describe(`findWebhook() - dynamic case ${tag}`, () => {
test('should return the webhook if found', async () => {
const method = 'GET';
const webhookId = uuid();
const path = 'user/:id/posts';
const mockWebhook = createWebhook(method, path, webhookId, 3);
webhookRepository.findOneBy.mockResolvedValue(null); // static
webhookRepository.findBy.mockResolvedValue([mockWebhook]); // dynamic
const returnedWebhook = await webhookService.findWebhook(
method,
[webhookId, 'user/123/posts'].join('/'),
);
expect(returnedWebhook).toBe(mockWebhook);
});
test('should handle subset dynamic path case', async () => {
const method1 = 'GET';
const webhookId1 = uuid();
const path1 = 'user/:id/posts';
const mockWebhook1 = createWebhook(method1, path1, webhookId1, 3);
const method2 = 'GET';
const webhookId2 = uuid();
const path2 = 'user/:id/posts/:postId/comments';
const mockWebhook2 = createWebhook(method2, path2, webhookId2, 3);
webhookRepository.findOneBy.mockResolvedValue(null); // static
webhookRepository.findBy.mockResolvedValue([mockWebhook1, mockWebhook2]); // dynamic
const fullPath1 = [webhookId1, 'user/123/posts'].join('/');
const returnedWebhook1 = await webhookService.findWebhook(method1, fullPath1);
const fullPath2 = [webhookId1, 'user/123/posts/456/comments'].join('/');
const returnedWebhook2 = await webhookService.findWebhook(method2, fullPath2);
expect(returnedWebhook1).toBe(mockWebhook1);
expect(returnedWebhook2).toBe(mockWebhook2);
});
test('should handle single-segment dynamic path case', async () => {
const method1 = 'GET';
const webhookId1 = uuid();
const path1 = ':var';
const mockWebhook1 = createWebhook(method1, path1, webhookId1, 3);
const method2 = 'GET';
const webhookId2 = uuid();
const path2 = 'user/:id/posts/:postId/comments';
const mockWebhook2 = createWebhook(method2, path2, webhookId2, 3);
webhookRepository.findOneBy.mockResolvedValue(null); // static
webhookRepository.findBy.mockResolvedValue([mockWebhook1, mockWebhook2]); // dynamic
const fullPath = [webhookId1, 'user/123/posts/456'].join('/');
const returnedWebhook = await webhookService.findWebhook(method1, fullPath);
expect(returnedWebhook).toBe(mockWebhook1);
});
test('should return null if not found', async () => {
const fullPath = [uuid(), 'user/:id/posts'].join('/');
webhookRepository.findOneBy.mockResolvedValue(null); // static
webhookRepository.findBy.mockResolvedValue([]); // dynamic
const returnValue = await webhookService.findWebhook('GET', fullPath);
expect(returnValue).toBeNull();
});
});
});
describe('getWebhookMethods()', () => {
test('should return all methods for webhook', async () => {
const path = 'user/profile';
webhookRepository.find.mockResolvedValue([
createWebhook('GET', path),
createWebhook('POST', path),
createWebhook('PUT', path),
createWebhook('PATCH', path),
]);
const returnedMethods = await webhookService.getWebhookMethods(path);
expect(returnedMethods).toEqual(['GET', 'POST', 'PUT', 'PATCH']);
});
test('should return empty array if no webhooks found', async () => {
webhookRepository.find.mockResolvedValue([]);
const returnedMethods = await webhookService.getWebhookMethods('user/profile');
expect(returnedMethods).toEqual([]);
});
});
describe('deleteWorkflowWebhooks()', () => {
test('should delete all webhooks of the workflow', async () => {
const mockWorkflowWebhooks = [
createWebhook('PUT', 'users'),
createWebhook('GET', 'user/:id'),
createWebhook('POST', ':var'),
];
webhookRepository.findBy.mockResolvedValue(mockWorkflowWebhooks);
const workflowId = uuid();
await webhookService.deleteWorkflowWebhooks(workflowId);
expect(webhookRepository.remove).toHaveBeenCalledWith(mockWorkflowWebhooks);
});
test('should not delete any webhooks if none found', async () => {
webhookRepository.findBy.mockResolvedValue([]);
const workflowId = uuid();
await webhookService.deleteWorkflowWebhooks(workflowId);
expect(webhookRepository.remove).toHaveBeenCalledWith([]);
});
});
describe('createWebhook()', () => {
test('should store webhook in DB', async () => {
const mockWebhook = createWebhook('GET', 'user/:id');
await webhookService.storeWebhook(mockWebhook);
expect(webhookRepository.upsert).toHaveBeenCalledWith(mockWebhook, ['method', 'webhookPath']);
});
});
describe('getNodeWebhooks()', () => {
const workflow = new Workflow({
id: 'test-workflow',
nodes: [],
connections: {},
active: true,
nodeTypes,
});
test('should return empty array if node is disabled', async () => {
const node = { disabled: true } as INode;
const webhooks = webhookService.getNodeWebhooks(workflow, node, additionalData);
expect(webhooks).toEqual([]);
});
test('should return webhooks for node with webhook definitions', async () => {
const node = {
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
disabled: false,
} as INode;
const nodeType = {
description: {
webhooks: [
{
name: 'default',
httpMethod: 'GET',
path: '/webhook',
isFullPath: false,
restartWebhook: false,
},
],
},
} as INodeType;
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
const webhooks = webhookService.getNodeWebhooks(workflow, node, additionalData);
expect(webhooks).toHaveLength(1);
expect(webhooks[0]).toMatchObject({
httpMethod: 'GET',
node: 'Webhook',
workflowId: 'test-workflow',
});
});
});
describe('createWebhookIfNotExists()', () => {
const workflow = new Workflow({
id: 'test-workflow',
nodes: [
mock<INode>({
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
typeVersion: 1,
parameters: {},
}),
],
connections: {},
active: false,
nodeTypes,
});
const webhookData = mock<IWebhookData>({
node: 'Webhook',
webhookDescription: {
name: 'default',
httpMethod: 'GET',
path: '/webhook',
},
});
const defaultWebhookMethods = {
checkExists: jest.fn(),
create: jest.fn(),
};
const nodeType = mock<INodeType>({
webhookMethods: { default: defaultWebhookMethods },
});
test('should create webhook if it does not exist', async () => {
defaultWebhookMethods.checkExists.mockResolvedValue(false);
defaultWebhookMethods.create.mockResolvedValue(true);
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
await webhookService.createWebhookIfNotExists(workflow, webhookData, 'trigger', 'init');
expect(defaultWebhookMethods.checkExists).toHaveBeenCalled();
expect(defaultWebhookMethods.create).toHaveBeenCalled();
});
test('should not create webhook if it already exists', async () => {
defaultWebhookMethods.checkExists.mockResolvedValue(true);
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
await webhookService.createWebhookIfNotExists(workflow, webhookData, 'trigger', 'init');
expect(defaultWebhookMethods.checkExists).toHaveBeenCalled();
expect(defaultWebhookMethods.create).not.toHaveBeenCalled();
});
test('should handle case when webhook methods are not defined', async () => {
nodeTypes.getByNameAndVersion.mockReturnValue({} as INodeType);
await webhookService.createWebhookIfNotExists(workflow, webhookData, 'trigger', 'init');
// Test passes if no error is thrown when webhook methods are undefined
});
});
describe('deleteWebhook()', () => {
test('should call runWebhookMethod with delete', async () => {
const workflow = mock<Workflow>();
const webhookData = mock<IWebhookData>();
const runWebhookMethodSpy = jest.spyOn(webhookService as any, 'runWebhookMethod');
await webhookService.deleteWebhook(workflow, webhookData, 'trigger', 'init');
expect(runWebhookMethodSpy).toHaveBeenCalledWith(
'delete',
workflow,
webhookData,
'trigger',
'init',
);
});
});
describe('runWebhook()', () => {
const workflow = mock<Workflow>();
const webhookData = mock<IWebhookData>();
const node = mock<INode>();
const responseData = { workflowData: [] };
test('should throw error if node does not have webhooks', async () => {
const nodeType = {} as INodeType;
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
await expect(
webhookService.runWebhook(workflow, webhookData, node, additionalData, 'trigger', null),
).rejects.toThrow('Node does not have any webhooks defined');
});
test('should execute webhook and return response data', async () => {
const nodeType = mock<INodeType>({
webhook: jest.fn().mockResolvedValue(responseData),
});
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
const result = await webhookService.runWebhook(
workflow,
webhookData,
node,
additionalData,
'trigger',
null,
);
expect(result).toEqual(responseData);
expect(nodeType.webhook).toHaveBeenCalled();
});
});
});