refactor(core): Reorganize webhook related components under src/webhooks (no-changelog) (#10296)
This commit is contained in:
@@ -1,134 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,142 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { WaitingWebhooks } from '@/WaitingWebhooks';
|
||||
import { ConflictError } from '@/errors/response-errors/conflict.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import type { IExecutionResponse, WaitingWebhookRequest } from '@/Interfaces';
|
||||
import type express from 'express';
|
||||
import type { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||
|
||||
describe('WaitingWebhooks', () => {
|
||||
const executionRepository = mock<ExecutionRepository>();
|
||||
const waitingWebhooks = new WaitingWebhooks(mock(), mock(), executionRepository);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should throw NotFoundError if there is no execution to resume', async () => {
|
||||
/**
|
||||
* Arrange
|
||||
*/
|
||||
executionRepository.findSingleExecution.mockResolvedValue(undefined);
|
||||
|
||||
/**
|
||||
* Act
|
||||
*/
|
||||
const promise = waitingWebhooks.executeWebhook(
|
||||
mock<WaitingWebhookRequest>(),
|
||||
mock<express.Response>(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Assert
|
||||
*/
|
||||
await expect(promise).rejects.toThrowError(NotFoundError);
|
||||
});
|
||||
|
||||
it('should throw ConflictError if the execution to resume is already running', async () => {
|
||||
/**
|
||||
* Arrange
|
||||
*/
|
||||
executionRepository.findSingleExecution.mockResolvedValue(
|
||||
mock<IExecutionResponse>({ status: 'running' }),
|
||||
);
|
||||
|
||||
/**
|
||||
* Act
|
||||
*/
|
||||
const promise = waitingWebhooks.executeWebhook(
|
||||
mock<WaitingWebhookRequest>(),
|
||||
mock<express.Response>(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Assert
|
||||
*/
|
||||
await expect(promise).rejects.toThrowError(ConflictError);
|
||||
});
|
||||
|
||||
it('should throw ConflictError if the execution to resume already finished', async () => {
|
||||
/**
|
||||
* Arrange
|
||||
*/
|
||||
executionRepository.findSingleExecution.mockResolvedValue(
|
||||
mock<IExecutionResponse>({ finished: true }),
|
||||
);
|
||||
|
||||
/**
|
||||
* Act
|
||||
*/
|
||||
const promise = waitingWebhooks.executeWebhook(
|
||||
mock<WaitingWebhookRequest>(),
|
||||
mock<express.Response>(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Assert
|
||||
*/
|
||||
await expect(promise).rejects.toThrowError(ConflictError);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user