mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(core): Fix test webhook deregistration (#8247)
This commit is contained in:
parent
90404a4b88
commit
5032bf0e34
|
@ -24,6 +24,7 @@ import { WebhookNotFoundError } from '@/errors/response-errors/webhook-not-found
|
||||||
import * as NodeExecuteFunctions from 'n8n-core';
|
import * as NodeExecuteFunctions from 'n8n-core';
|
||||||
import { removeTrailingSlash } from './utils';
|
import { removeTrailingSlash } from './utils';
|
||||||
import { TestWebhookRegistrationsService } from '@/services/test-webhook-registrations.service';
|
import { TestWebhookRegistrationsService } from '@/services/test-webhook-registrations.service';
|
||||||
|
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class TestWebhooks implements IWebhookManager {
|
export class TestWebhooks implements IWebhookManager {
|
||||||
|
@ -185,6 +186,7 @@ export class TestWebhooks implements IWebhookManager {
|
||||||
* For every webhook call to listen for, also activate the webhook.
|
* For every webhook call to listen for, also activate the webhook.
|
||||||
*/
|
*/
|
||||||
async needsWebhook(
|
async needsWebhook(
|
||||||
|
userId: string,
|
||||||
workflowEntity: IWorkflowDb,
|
workflowEntity: IWorkflowDb,
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
sessionId?: string,
|
sessionId?: string,
|
||||||
|
@ -219,21 +221,23 @@ export class TestWebhooks implements IWebhookManager {
|
||||||
webhook.isTest = true;
|
webhook.isTest = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove additional data from webhook because:
|
* Additional data cannot be cached because of circular refs.
|
||||||
*
|
* Hence store the `userId` and recreate additional data when needed.
|
||||||
* - It is not needed for the test webhook to be executed.
|
|
||||||
* - It contains circular refs that cannot be cached.
|
|
||||||
*/
|
*/
|
||||||
const { workflowExecuteAdditionalData: _, ...rest } = webhook;
|
const { workflowExecuteAdditionalData: _, ...cacheableWebhook } = webhook;
|
||||||
|
|
||||||
|
cacheableWebhook.userId = userId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await workflow.createWebhookIfNotExists(webhook, NodeExecuteFunctions, 'manual', 'manual');
|
await workflow.createWebhookIfNotExists(webhook, NodeExecuteFunctions, 'manual', 'manual');
|
||||||
|
|
||||||
|
cacheableWebhook.staticData = workflow.staticData;
|
||||||
|
|
||||||
await this.registrations.register({
|
await this.registrations.register({
|
||||||
sessionId,
|
sessionId,
|
||||||
workflowEntity,
|
workflowEntity,
|
||||||
destinationNode,
|
destinationNode,
|
||||||
webhook: rest as IWebhookData,
|
webhook: cacheableWebhook as IWebhookData,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.timeouts[key] = timeout;
|
this.timeouts[key] = timeout;
|
||||||
|
@ -341,6 +345,14 @@ export class TestWebhooks implements IWebhookManager {
|
||||||
if (!webhooks) return; // nothing to deactivate
|
if (!webhooks) return; // nothing to deactivate
|
||||||
|
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
|
const { userId, staticData } = webhook;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
webhook.workflowExecuteAdditionalData = await WorkflowExecuteAdditionalData.getBase(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (staticData) workflow.staticData = staticData;
|
||||||
|
|
||||||
await workflow.deleteWebhook(webhook, NodeExecuteFunctions, 'internal', 'update');
|
await workflow.deleteWebhook(webhook, NodeExecuteFunctions, 'internal', 'update');
|
||||||
|
|
||||||
await this.registrations.deregister(webhook);
|
await this.registrations.deregister(webhook);
|
||||||
|
@ -350,7 +362,7 @@ export class TestWebhooks implements IWebhookManager {
|
||||||
/**
|
/**
|
||||||
* Convert a `WorkflowEntity` from `typeorm` to a `Workflow` from `n8n-workflow`.
|
* Convert a `WorkflowEntity` from `typeorm` to a `Workflow` from `n8n-workflow`.
|
||||||
*/
|
*/
|
||||||
private toWorkflow(workflowEntity: IWorkflowDb) {
|
toWorkflow(workflowEntity: IWorkflowDb) {
|
||||||
return new Workflow({
|
return new Workflow({
|
||||||
id: workflowEntity.id,
|
id: workflowEntity.id,
|
||||||
name: workflowEntity.name,
|
name: workflowEntity.name,
|
||||||
|
@ -358,7 +370,14 @@ export class TestWebhooks implements IWebhookManager {
|
||||||
connections: workflowEntity.connections,
|
connections: workflowEntity.connections,
|
||||||
active: false,
|
active: false,
|
||||||
nodeTypes: this.nodeTypes,
|
nodeTypes: this.nodeTypes,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `staticData` in the original workflow entity has production webhook IDs.
|
||||||
|
* Since we are creating here a temporary workflow only for a test webhook,
|
||||||
|
* `staticData` from the original workflow entity should not be transferred.
|
||||||
|
*/
|
||||||
staticData: undefined,
|
staticData: undefined,
|
||||||
|
|
||||||
settings: workflowEntity.settings,
|
settings: workflowEntity.settings,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -317,6 +317,7 @@ export class WorkflowService {
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(user.id);
|
const additionalData = await WorkflowExecuteAdditionalData.getBase(user.id);
|
||||||
|
|
||||||
const needsWebhook = await this.testWebhooks.needsWebhook(
|
const needsWebhook = await this.testWebhooks.needsWebhook(
|
||||||
|
user.id,
|
||||||
workflowData,
|
workflowData,
|
||||||
additionalData,
|
additionalData,
|
||||||
sessionId,
|
sessionId,
|
||||||
|
|
|
@ -5,53 +5,49 @@ import { v4 as uuid } from 'uuid';
|
||||||
import { generateNanoId } from '@/databases/utils/generators';
|
import { generateNanoId } from '@/databases/utils/generators';
|
||||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
import * as WebhookHelpers from '@/WebhookHelpers';
|
import * as WebhookHelpers from '@/WebhookHelpers';
|
||||||
|
import type * as express from 'express';
|
||||||
|
|
||||||
import type { IWorkflowDb, WebhookRequest } from '@/Interfaces';
|
import type { IWorkflowDb, WebhookRequest } from '@/Interfaces';
|
||||||
import type {
|
import type { IWebhookData, IWorkflowExecuteAdditionalData } from 'n8n-workflow';
|
||||||
IWebhookData,
|
|
||||||
IWorkflowExecuteAdditionalData,
|
|
||||||
Workflow,
|
|
||||||
WorkflowActivateMode,
|
|
||||||
WorkflowExecuteMode,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import type {
|
import type {
|
||||||
TestWebhookRegistrationsService,
|
TestWebhookRegistrationsService,
|
||||||
TestWebhookRegistration,
|
TestWebhookRegistration,
|
||||||
} from '@/services/test-webhook-registrations.service';
|
} from '@/services/test-webhook-registrations.service';
|
||||||
|
|
||||||
describe('TestWebhooks', () => {
|
import * as AdditionalData from '@/WorkflowExecuteAdditionalData';
|
||||||
const registrations = mock<TestWebhookRegistrationsService>();
|
|
||||||
const testWebhooks = new TestWebhooks(mock(), mock(), registrations);
|
|
||||||
|
|
||||||
|
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(() => {
|
beforeAll(() => {
|
||||||
|
testWebhooks = new TestWebhooks(mock(), mock(), registrations);
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
const httpMethod = 'GET';
|
|
||||||
const path = uuid();
|
|
||||||
const workflowId = generateNanoId();
|
|
||||||
|
|
||||||
const webhook = mock<IWebhookData>({
|
|
||||||
httpMethod,
|
|
||||||
path,
|
|
||||||
workflowId,
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('needsWebhook()', () => {
|
describe('needsWebhook()', () => {
|
||||||
type NeedsWebhookArgs = [
|
const args: Parameters<typeof testWebhooks.needsWebhook> = [
|
||||||
IWorkflowDb,
|
userId,
|
||||||
IWorkflowExecuteAdditionalData,
|
workflowEntity,
|
||||||
WorkflowExecuteMode,
|
|
||||||
WorkflowActivateMode,
|
|
||||||
];
|
|
||||||
|
|
||||||
const workflow = mock<Workflow>({ id: workflowId });
|
|
||||||
|
|
||||||
const args: NeedsWebhookArgs = [
|
|
||||||
mock<IWorkflowDb>({ id: workflowId, nodes: [] }),
|
|
||||||
mock<IWorkflowExecuteAdditionalData>(),
|
mock<IWorkflowExecuteAdditionalData>(),
|
||||||
'manual',
|
|
||||||
'manual',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
test('if webhook is needed, should return true and activate webhook', async () => {
|
test('if webhook is needed, should return true and activate webhook', async () => {
|
||||||
|
@ -103,17 +99,29 @@ describe('TestWebhooks', () => {
|
||||||
|
|
||||||
const registration = mock<TestWebhookRegistration>({
|
const registration = mock<TestWebhookRegistration>({
|
||||||
sessionId: 'some-session-id',
|
sessionId: 'some-session-id',
|
||||||
workflowEntity: mock<IWorkflowDb>({}),
|
workflowEntity,
|
||||||
});
|
});
|
||||||
|
|
||||||
await registrations.register(registration);
|
await registrations.register(registration);
|
||||||
|
|
||||||
const promise = testWebhooks.executeWebhook(
|
const promise = testWebhooks.executeWebhook(
|
||||||
mock<WebhookRequest>({ params: { path } }),
|
mock<WebhookRequest>({ params: { path } }),
|
||||||
mock(),
|
mock<express.Response>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(promise).rejects.toThrowError(NotFoundError);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1674,6 +1674,8 @@ export interface IWebhookData {
|
||||||
workflowExecuteAdditionalData: IWorkflowExecuteAdditionalData;
|
workflowExecuteAdditionalData: IWorkflowExecuteAdditionalData;
|
||||||
webhookId?: string;
|
webhookId?: string;
|
||||||
isTest?: boolean;
|
isTest?: boolean;
|
||||||
|
userId?: string;
|
||||||
|
staticData?: Workflow['staticData'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWebhookDescription {
|
export interface IWebhookDescription {
|
||||||
|
|
Loading…
Reference in a new issue