refactor(core): Simplify state in test webhooks (no-changelog) (#8155)

This PR simplifies state in test webhooks so that it can be cached
easily. Caching this state will allow us to start using Redis for manual
webhooks, to support manual webhooks to work in multi-main setup.

- [x] Convert `workflowWebhooks` to a getter - no need to optimize for
deactivation
- [x] Remove array from value in `TestWebhooks.webhookUrls`
- [x] Consolidate `webhookUrls` and `registeredWebhooks`
This commit is contained in:
Iván Ovejero
2023-12-28 09:28:12 +01:00
committed by GitHub
parent 0e582594ea
commit 639afcd7a5
5 changed files with 141 additions and 180 deletions

View File

@@ -1,6 +1,4 @@
import { mockInstance } from '../shared/mocking';
import { NodeTypes } from '@/NodeTypes';
import { Push } from '@/push';
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';
@@ -8,8 +6,7 @@ import { generateNanoId } from '@/databases/utils/generators';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import * as WebhookHelpers from '@/WebhookHelpers';
import type { IWorkflowDb, WebhookRequest } from '@/Interfaces';
import type express from 'express';
import type { IWorkflowDb, WebhookRegistration, WebhookRequest } from '@/Interfaces';
import type {
IWebhookData,
IWorkflowExecuteAdditionalData,
@@ -19,31 +16,27 @@ import type {
} from 'n8n-workflow';
describe('TestWebhooks', () => {
jest.useFakeTimers();
const testWebhooks = new TestWebhooks(mock(), mock());
const push = mockInstance(Push);
const nodeTypes = mockInstance(NodeTypes);
const testWebhooks = new TestWebhooks(push, nodeTypes);
beforeAll(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllMocks();
testWebhooks.clearRegistrations();
});
const httpMethod = 'GET';
const path = uuid();
const workflowId = generateNanoId();
const webhook = mock<IWebhookData>({
httpMethod,
path,
workflowId,
});
describe('needsWebhook()', () => {
const httpMethod = 'GET';
const path = uuid();
const workflowId = generateNanoId();
const webhook = {
httpMethod,
path,
workflowId,
webhookDescription: {},
} as IWebhookData;
const keyPart = [httpMethod, path].join('|');
type NeedsWebhookArgs = [
IWorkflowDb,
Workflow,
@@ -52,23 +45,18 @@ describe('TestWebhooks', () => {
WorkflowActivateMode,
];
const workflow = {
id: workflowId,
createWebhookIfNotExists: () => {},
deleteWebhook: () => {},
} as unknown as Workflow;
const workflow = mock<Workflow>({ id: workflowId });
const args: NeedsWebhookArgs = [
{ id: workflowId } as unknown as IWorkflowDb,
mock<IWorkflowDb>({ id: workflowId }),
workflow,
{} as unknown as IWorkflowExecuteAdditionalData,
mock<IWorkflowExecuteAdditionalData>(),
'manual',
'manual',
];
test('should register a webhook as active', async () => {
test('should return true and activate webhook if needed', async () => {
jest.spyOn(WebhookHelpers, 'getWorkflowWebhooks').mockReturnValue([webhook]);
jest.spyOn(testWebhooks, 'toWebhookKey').mockReturnValue(keyPart);
const activateWebhookSpy = jest.spyOn(testWebhooks, 'activateWebhook');
const needsWebhook = await testWebhooks.needsWebhook(...args);
@@ -77,18 +65,17 @@ describe('TestWebhooks', () => {
expect(activateWebhookSpy).toHaveBeenCalledWith(workflow, webhook, 'manual', 'manual');
});
test('should remove from active webhooks on failure to add', async () => {
test('should deactivate webhooks on failure to activate', async () => {
const msg = 'Failed to add webhook to active webhooks';
jest.spyOn(WebhookHelpers, 'getWorkflowWebhooks').mockReturnValue([webhook]);
jest.spyOn(testWebhooks, 'toWebhookKey').mockReturnValue(keyPart);
jest.spyOn(testWebhooks, 'activateWebhook').mockRejectedValue(new Error(msg));
const deactivateSpy = jest.spyOn(testWebhooks, 'deactivateWebhooksFor');
const deactivateWebhooksSpy = jest.spyOn(testWebhooks, 'deactivateWebhooks');
const needsWebhook = testWebhooks.needsWebhook(...args);
await expect(needsWebhook).rejects.toThrowError(msg);
expect(deactivateSpy).toHaveBeenCalledWith(workflow);
expect(deactivateWebhooksSpy).toHaveBeenCalledWith(workflow);
});
test('should return false if no webhook to start workflow', async () => {
@@ -102,26 +89,14 @@ describe('TestWebhooks', () => {
});
describe('executeWebhook()', () => {
const httpMethod = 'GET';
const path = uuid();
const workflowId = generateNanoId();
const webhook = {
httpMethod,
path,
workflowId,
} as IWebhookData;
const keyPart = [httpMethod, path].join('|');
test('should throw if webhook is not registered', async () => {
jest.spyOn(testWebhooks, 'getActiveWebhook').mockReturnValue(webhook);
jest.spyOn(testWebhooks, 'getWebhookMethods').mockResolvedValue([]);
jest.spyOn(testWebhooks, 'toWebhookKey').mockReturnValue(keyPart);
const request = { params: { path } } as WebhookRequest;
const response = {} as express.Response;
const promise = testWebhooks.executeWebhook(request, response);
const promise = testWebhooks.executeWebhook(
mock<WebhookRequest>({ params: { path } }),
mock(),
);
await expect(promise).rejects.toThrowError(WebhookNotFoundError);
});
@@ -129,26 +104,22 @@ describe('TestWebhooks', () => {
test('should throw if webhook node is registered but missing from workflow', async () => {
jest.spyOn(testWebhooks, 'getActiveWebhook').mockReturnValue(webhook);
jest.spyOn(testWebhooks, 'getWebhookMethods').mockResolvedValue([]);
jest.spyOn(testWebhooks, 'toWebhookKey').mockReturnValue(keyPart);
// @ts-expect-error Private property
testWebhooks.registeredWebhooks[`${keyPart}|${workflowId}`] = {
const registration = mock<WebhookRegistration>({
sessionId: 'some-session-id',
timeout: setTimeout(() => {}, 0),
workflowEntity: {} as IWorkflowDb,
workflow: {
getNode: () => null,
} as unknown as Workflow,
};
timeout: mock<NodeJS.Timeout>(),
workflowEntity: mock<IWorkflowDb>({}),
workflow: mock<Workflow>(),
});
const request = { params: { path } } as WebhookRequest;
const response = {} as express.Response;
const promise = testWebhooks.executeWebhook(request, response);
testWebhooks.setRegistration(registration);
const promise = testWebhooks.executeWebhook(
mock<WebhookRequest>({ params: { path } }),
mock(),
);
await expect(promise).rejects.toThrowError(NotFoundError);
// @ts-expect-error Private property
delete testWebhooks.registeredWebhooks[`${keyPart}|${workflowId}`];
});
});
});