mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
fix(core): Fix getting webhook methods from path only when dynamic webhook path (#17803)
This commit is contained in:
committed by
GitHub
parent
6c1352f75c
commit
a5c6e2fecf
@@ -1,3 +1,4 @@
|
|||||||
|
import type { WorkflowEntity } from '@n8n/db';
|
||||||
import { generateNanoId } from '@n8n/db';
|
import { generateNanoId } from '@n8n/db';
|
||||||
import type * as express from 'express';
|
import type * as express from 'express';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
@@ -7,6 +8,7 @@ import type {
|
|||||||
IWebhookData,
|
IWebhookData,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
Workflow,
|
Workflow,
|
||||||
|
IHttpRequestMethods,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
@@ -195,11 +197,40 @@ describe('TestWebhooks', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getWebhookMethods()', () => {
|
describe('getWebhookMethods()', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
registrations.toKey.mockImplementation(
|
||||||
|
(webhook: Pick<IWebhookData, 'webhookId' | 'httpMethod' | 'path'>) => {
|
||||||
|
const { webhookId, httpMethod, path: webhookPath } = webhook;
|
||||||
|
if (!webhookId) return [httpMethod, webhookPath].join('|');
|
||||||
|
|
||||||
|
let path = webhookPath;
|
||||||
|
if (path.startsWith(webhookId)) {
|
||||||
|
const cutFromIndex = path.indexOf('/') + 1;
|
||||||
|
|
||||||
|
path = path.slice(cutFromIndex);
|
||||||
|
}
|
||||||
|
return [httpMethod, webhookId, path.split('/').length].join('|');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('should normalize trailing slash', async () => {
|
test('should normalize trailing slash', async () => {
|
||||||
const METHOD = 'POST';
|
const METHOD = 'POST';
|
||||||
const PATH_WITH_SLASH = 'register/';
|
const PATH_WITH_SLASH = 'register/';
|
||||||
const PATH_WITHOUT_SLASH = 'register';
|
const PATH_WITHOUT_SLASH = 'register';
|
||||||
registrations.getAllKeys.mockResolvedValue([`${METHOD}|${PATH_WITHOUT_SLASH}`]);
|
const webhookData = {
|
||||||
|
httpMethod: METHOD as IHttpRequestMethods,
|
||||||
|
path: PATH_WITHOUT_SLASH,
|
||||||
|
} as IWebhookData;
|
||||||
|
|
||||||
|
registrations.getRegistrationsHash.mockImplementation(async () => {
|
||||||
|
return {
|
||||||
|
[registrations.toKey(webhookData)]: {
|
||||||
|
workflowEntity: mock<WorkflowEntity>(),
|
||||||
|
webhook: webhookData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const resultWithSlash = await testWebhooks.getWebhookMethods(PATH_WITH_SLASH);
|
const resultWithSlash = await testWebhooks.getWebhookMethods(PATH_WITH_SLASH);
|
||||||
const resultWithoutSlash = await testWebhooks.getWebhookMethods(PATH_WITHOUT_SLASH);
|
const resultWithoutSlash = await testWebhooks.getWebhookMethods(PATH_WITHOUT_SLASH);
|
||||||
@@ -207,5 +238,30 @@ describe('TestWebhooks', () => {
|
|||||||
expect(resultWithSlash).toEqual([METHOD]);
|
expect(resultWithSlash).toEqual([METHOD]);
|
||||||
expect(resultWithoutSlash).toEqual([METHOD]);
|
expect(resultWithoutSlash).toEqual([METHOD]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should return methods for webhooks with dynamic paths', async () => {
|
||||||
|
const METHOD = 'POST';
|
||||||
|
const PATH = '12345/register/:id';
|
||||||
|
|
||||||
|
const webhookData = {
|
||||||
|
webhookId: '12345',
|
||||||
|
httpMethod: METHOD as IHttpRequestMethods,
|
||||||
|
// Path for dynamic webhook does not contain webhookId
|
||||||
|
path: 'register/:id',
|
||||||
|
};
|
||||||
|
|
||||||
|
registrations.getRegistrationsHash.mockImplementation(async () => {
|
||||||
|
return {
|
||||||
|
[registrations.toKey(webhookData)]: {
|
||||||
|
workflowEntity: mock<WorkflowEntity>(),
|
||||||
|
webhook: webhookData as IWebhookData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await testWebhooks.getWebhookMethods(PATH);
|
||||||
|
|
||||||
|
expect(result).toEqual([METHOD]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -154,6 +154,21 @@ describe('WebhookService', () => {
|
|||||||
|
|
||||||
expect(returnedMethods).toEqual([]);
|
expect(returnedMethods).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should return dynamic webhook method when static search returns nothing', async () => {
|
||||||
|
const webhookId = uuid();
|
||||||
|
const dynamicPath = `${webhookId}/user/1`;
|
||||||
|
const mockDynamicWebhook = createWebhook('POST', 'user/:id', webhookId, 2);
|
||||||
|
|
||||||
|
// Mock static webhook search to return empty
|
||||||
|
webhookRepository.find.mockResolvedValue([]);
|
||||||
|
// Mock dynamic webhook search to return a webhook
|
||||||
|
webhookRepository.findBy.mockResolvedValue([mockDynamicWebhook]);
|
||||||
|
|
||||||
|
const returnedMethods = await webhookService.getWebhookMethods(dynamicPath);
|
||||||
|
|
||||||
|
expect(returnedMethods).toEqual(['POST']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteWorkflowWebhooks()', () => {
|
describe('deleteWorkflowWebhooks()', () => {
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ export class TestWebhookRegistrationsService {
|
|||||||
return Object.values(hash);
|
return Object.values(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRegistrationsHash() {
|
||||||
|
return await this.cacheService.getHash<TestWebhookRegistration>(this.cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
async deregisterAll() {
|
async deregisterAll() {
|
||||||
await this.cacheService.delete(this.cacheKey);
|
await this.cacheService.delete(this.cacheKey);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,13 +200,34 @@ export class TestWebhooks implements IWebhookManager {
|
|||||||
if (timeout) clearTimeout(timeout);
|
if (timeout) clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getWebhooksFromPath(rawPath: string) {
|
||||||
|
const path = removeTrailingSlash(rawPath);
|
||||||
|
const webhooks: IWebhookData[] = [];
|
||||||
|
const registrations = await this.registrations.getRegistrationsHash();
|
||||||
|
|
||||||
|
for (const httpMethod of ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as IHttpRequestMethods[]) {
|
||||||
|
const key = this.registrations.toKey({ httpMethod, path });
|
||||||
|
let webhook = registrations?.[key]?.webhook;
|
||||||
|
if (!webhook) {
|
||||||
|
// check for dynamic webhooks
|
||||||
|
const [webhookId, ...segments] = path.split('/');
|
||||||
|
const key = this.registrations.toKey({ httpMethod, path, webhookId });
|
||||||
|
if (registrations?.[key]) {
|
||||||
|
webhook = this.getActiveWebhookFromRegistration(segments.join('/'), registrations?.[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (webhook) {
|
||||||
|
webhooks.push(webhook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return webhooks;
|
||||||
|
}
|
||||||
|
|
||||||
async getWebhookMethods(rawPath: string) {
|
async getWebhookMethods(rawPath: string) {
|
||||||
const path = removeTrailingSlash(rawPath);
|
const path = removeTrailingSlash(rawPath);
|
||||||
const allKeys = await this.registrations.getAllKeys();
|
const webhooks = await this.getWebhooksFromPath(path);
|
||||||
|
|
||||||
const webhookMethods = allKeys
|
const webhookMethods = webhooks.map((webhook) => webhook.httpMethod);
|
||||||
.filter((key) => key.includes(path))
|
|
||||||
.map((key) => key.split('|')[0] as IHttpRequestMethods);
|
|
||||||
|
|
||||||
if (!webhookMethods.length) throw new WebhookNotFoundError({ path });
|
if (!webhookMethods.length) throw new WebhookNotFoundError({ path });
|
||||||
|
|
||||||
@@ -393,33 +414,31 @@ export class TestWebhooks implements IWebhookManager {
|
|||||||
return foundWebhook;
|
return foundWebhook;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActiveWebhook(httpMethod: IHttpRequestMethods, path: string, webhookId?: string) {
|
getActiveWebhookFromRegistration(
|
||||||
const key = this.registrations.toKey({ httpMethod, path, webhookId });
|
path: string,
|
||||||
|
registration: TestWebhookRegistration,
|
||||||
let webhook: IWebhookData | undefined;
|
): IWebhookData | undefined {
|
||||||
let maxMatches = 0;
|
|
||||||
const pathElementsSet = new Set(path.split('/'));
|
const pathElementsSet = new Set(path.split('/'));
|
||||||
// check if static elements match in path
|
|
||||||
// if more results have been returned choose the one with the most static-route matches
|
|
||||||
const registration = await this.registrations.get(key);
|
|
||||||
|
|
||||||
if (!registration) return;
|
|
||||||
|
|
||||||
const { webhook: dynamicWebhook } = registration;
|
const { webhook: dynamicWebhook } = registration;
|
||||||
|
|
||||||
const staticElements = dynamicWebhook.path.split('/').filter((ele) => !ele.startsWith(':'));
|
const staticElements = dynamicWebhook.path.split('/').filter((ele) => !ele.startsWith(':'));
|
||||||
const allStaticExist = staticElements.every((staticEle) => pathElementsSet.has(staticEle));
|
const allStaticExist = staticElements.every((staticEle) => pathElementsSet.has(staticEle));
|
||||||
|
|
||||||
if (allStaticExist && staticElements.length > maxMatches) {
|
// webhook matches if all static elements exist or if there are no static elements
|
||||||
maxMatches = staticElements.length;
|
if ((allStaticExist && staticElements.length > 0) || staticElements.length === 0) {
|
||||||
webhook = dynamicWebhook;
|
return dynamicWebhook;
|
||||||
}
|
}
|
||||||
// handle routes with no static elements
|
return undefined;
|
||||||
else if (staticElements.length === 0 && !webhook) {
|
|
||||||
webhook = dynamicWebhook;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return webhook;
|
async getActiveWebhook(httpMethod: IHttpRequestMethods, path: string, webhookId?: string) {
|
||||||
|
const key = this.registrations.toKey({ httpMethod, path, webhookId });
|
||||||
|
const registration = await this.registrations.get(key);
|
||||||
|
|
||||||
|
if (!registration) return;
|
||||||
|
|
||||||
|
return this.getActiveWebhookFromRegistration(path, registration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export class WebhookService {
|
|||||||
return dbStaticWebhook;
|
return dbStaticWebhook;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.findDynamicWebhook(method, path);
|
return await this.findDynamicWebhook(path, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,7 +71,7 @@ export class WebhookService {
|
|||||||
* Find a matching webhook with one or more dynamic path segments, e.g. `<uuid>/user/:id/posts`.
|
* Find a matching webhook with one or more dynamic path segments, e.g. `<uuid>/user/:id/posts`.
|
||||||
* It is mandatory for dynamic webhooks to have `<uuid>/` at the base.
|
* It is mandatory for dynamic webhooks to have `<uuid>/` at the base.
|
||||||
*/
|
*/
|
||||||
private async findDynamicWebhook(method: Method, path: string) {
|
private async findDynamicWebhook(path: string, method?: Method) {
|
||||||
const [uuidSegment, ...otherSegments] = path.split('/');
|
const [uuidSegment, ...otherSegments] = path.split('/');
|
||||||
|
|
||||||
const dynamicWebhooks = await this.webhookRepository.findBy({
|
const dynamicWebhooks = await this.webhookRepository.findBy({
|
||||||
@@ -133,10 +133,19 @@ export class WebhookService {
|
|||||||
return await this.webhookRepository.remove(webhooks);
|
return await this.webhookRepository.remove(webhooks);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWebhookMethods(path: string) {
|
async getWebhookMethods(rawPath: string) {
|
||||||
return await this.webhookRepository
|
// Try to find static webhooks first
|
||||||
.find({ select: ['method'], where: { webhookPath: path } })
|
const staticMethods = await this.webhookRepository
|
||||||
|
.find({ select: ['method'], where: { webhookPath: rawPath } })
|
||||||
.then((rows) => rows.map((r) => r.method));
|
.then((rows) => rows.map((r) => r.method));
|
||||||
|
|
||||||
|
if (staticMethods.length > 0) {
|
||||||
|
return staticMethods;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, try to find dynamic webhooks based on path only
|
||||||
|
const dynamicWebhooks = await this.findDynamicWebhook(rawPath);
|
||||||
|
return dynamicWebhooks ? [dynamicWebhooks.method] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private isDynamicPath(rawPath: string) {
|
private isDynamicPath(rawPath: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user