mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
fix(core, editor): Move single webhook trigger check to the backend (#22450)
Co-authored-by: Danny Martini <danny@n8n.io>
This commit is contained in:
@@ -981,4 +981,16 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||
|
||||
return await qb.getMany();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the workflow is stored as `active`.
|
||||
*
|
||||
* @important Do not confuse with `ActiveWorkflows.isActive()`,
|
||||
* which checks if the workflow is active in memory.
|
||||
*/
|
||||
async isActive(workflowId: string) {
|
||||
const workflow = await this.findOne({ select: ['activeVersionId'], where: { id: workflowId } });
|
||||
|
||||
return !!workflow?.activeVersionId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,21 +138,6 @@ export class ActiveWorkflowManager {
|
||||
return this.activeWorkflows.allActiveWorkflows();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the workflow is stored as `active`.
|
||||
*
|
||||
* @important Do not confuse with `ActiveWorkflows.isActive()`,
|
||||
* which checks if the workflow is active in memory.
|
||||
*/
|
||||
async isActive(workflowId: WorkflowId) {
|
||||
const workflow = await this.workflowRepository.findOne({
|
||||
select: ['activeVersionId'],
|
||||
where: { id: workflowId },
|
||||
});
|
||||
|
||||
return !!workflow?.activeVersionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register workflow-defined webhooks in the `workflow_entity` table.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { UserError } from 'n8n-workflow';
|
||||
|
||||
export class SingleWebhookTriggerError extends UserError {
|
||||
constructor(triggerName: string) {
|
||||
super(
|
||||
`Because of limitations in ${triggerName}, n8n can't listen for test executions at the same time as listening for production ones. Unpublish the workflow to execute.`,
|
||||
{ extra: { triggerName } },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -149,6 +149,72 @@ describe('TestWebhooks', () => {
|
||||
expect(webhookService.createWebhookIfNotExists.mock.calls[0][1].node).toBe(webhook2.node);
|
||||
expect(needsWebhook).toBe(true);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ published: true, withSingleWebhookTrigger: true, shouldThrow: true },
|
||||
{ published: true, withSingleWebhookTrigger: false, shouldThrow: false },
|
||||
{ published: false, withSingleWebhookTrigger: true, shouldThrow: false },
|
||||
{ published: false, withSingleWebhookTrigger: false, shouldThrow: false },
|
||||
] satisfies Array<{
|
||||
published: boolean;
|
||||
withSingleWebhookTrigger: boolean;
|
||||
shouldThrow: boolean;
|
||||
}>)(
|
||||
'handles single webhook trigger when workflowIsActive=%s',
|
||||
async ({ published: workflowIsActive, withSingleWebhookTrigger, shouldThrow }) => {
|
||||
const workflow = mock<Workflow>();
|
||||
const regularWebhook = mock<IWebhookData>({
|
||||
node: 'Webhook',
|
||||
httpMethod,
|
||||
path: 'regular-path',
|
||||
workflowId: workflowEntity.id,
|
||||
userId,
|
||||
});
|
||||
const telegramWebhook = mock<IWebhookData>({
|
||||
node: 'Telegram Trigger',
|
||||
httpMethod,
|
||||
path: 'telegram-path',
|
||||
workflowId: workflowEntity.id,
|
||||
userId,
|
||||
});
|
||||
const webhookNode = mock<IWorkflowBase['nodes'][number]>({
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
});
|
||||
const telegramNode = mock<IWorkflowBase['nodes'][number]>({
|
||||
name: 'Telegram Trigger',
|
||||
type: 'n8n-nodes-base.telegramTrigger',
|
||||
});
|
||||
|
||||
jest.spyOn(testWebhooks, 'toWorkflow').mockReturnValueOnce(workflow);
|
||||
jest
|
||||
.spyOn(WebhookHelpers, 'getWorkflowWebhooks')
|
||||
.mockReturnValue([regularWebhook, telegramWebhook]);
|
||||
jest.spyOn(workflow, 'getNode').mockImplementation((name: string) => {
|
||||
if (name === 'Webhook') return webhookNode;
|
||||
if (name === 'Telegram Trigger' && withSingleWebhookTrigger) return telegramNode;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (shouldThrow) {
|
||||
const promise = testWebhooks.needsWebhook({
|
||||
...args,
|
||||
workflowIsActive,
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow(
|
||||
"Because of limitations in Telegram Trigger, n8n can't listen for test executions at the same time as listening for production ones. Unpublish the workflow to execute.",
|
||||
);
|
||||
} else {
|
||||
const needsWebhook = await testWebhooks.needsWebhook({
|
||||
...args,
|
||||
workflowIsActive,
|
||||
});
|
||||
|
||||
expect(needsWebhook).toBe(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('executeWebhook()', () => {
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { WebhookNotFoundError } from '@/errors/response-errors/webhook-not-found.error';
|
||||
import { SingleWebhookTriggerError } from '@/errors/single-webhook-trigger.error';
|
||||
import { WorkflowMissingIdError } from '@/errors/workflow-missing-id.error';
|
||||
import { NodeTypes } from '@/node-types';
|
||||
import { Push } from '@/push';
|
||||
@@ -36,6 +37,12 @@ import * as WebhookHelpers from '@/webhooks/webhook-helpers';
|
||||
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
|
||||
import type { WorkflowRequest } from '@/workflows/workflow.request';
|
||||
|
||||
const SINGLE_WEBHOOK_TRIGGERS = [
|
||||
'n8n-nodes-base.telegramTrigger',
|
||||
'n8n-nodes-base.slackTrigger',
|
||||
'n8n-nodes-base.facebookLeadAdsTrigger',
|
||||
];
|
||||
|
||||
/**
|
||||
* Service for handling the execution of webhooks of manual executions
|
||||
* that use the [Test URL](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/#webhook-urls).
|
||||
@@ -272,6 +279,7 @@ export class TestWebhooks implements IWebhookManager {
|
||||
pushRef?: string;
|
||||
destinationNode?: IDestinationNode;
|
||||
triggerToStartFrom?: WorkflowRequest.FullManualExecutionFromKnownTriggerPayload['triggerToStartFrom'];
|
||||
workflowIsActive?: boolean;
|
||||
}) {
|
||||
const {
|
||||
userId,
|
||||
@@ -281,6 +289,7 @@ export class TestWebhooks implements IWebhookManager {
|
||||
pushRef,
|
||||
destinationNode,
|
||||
triggerToStartFrom,
|
||||
workflowIsActive,
|
||||
} = options;
|
||||
|
||||
if (!workflowEntity.id) throw new WorkflowMissingIdError(workflowEntity);
|
||||
@@ -310,6 +319,18 @@ export class TestWebhooks implements IWebhookManager {
|
||||
return false; // no webhooks found to start a workflow
|
||||
}
|
||||
|
||||
// Check if any webhook is a single webhook trigger and workflow is active
|
||||
if (workflowIsActive) {
|
||||
const singleWebhookTrigger = webhooks.find((w) =>
|
||||
SINGLE_WEBHOOK_TRIGGERS.includes(workflow.getNode(w.node)?.type ?? ''),
|
||||
);
|
||||
if (singleWebhookTrigger) {
|
||||
throw new SingleWebhookTriggerError(
|
||||
workflow.getNode(singleWebhookTrigger.node)?.name ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const timeout = setTimeout(
|
||||
async () => await this.cancelWebhook(workflow.id),
|
||||
TEST_WEBHOOK_TIMEOUT,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
|
||||
import type { IWorkflowErrorData } from '@/interfaces';
|
||||
import type { NodeTypes } from '@/node-types';
|
||||
import type { TestWebhooks } from '@/webhooks/test-webhooks';
|
||||
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
|
||||
import type { WorkflowRunner } from '@/workflow-runner';
|
||||
import { WorkflowExecutionService } from '@/workflows/workflow-execution.service';
|
||||
@@ -336,6 +337,66 @@ describe('WorkflowExecutionService', () => {
|
||||
expect(callArgs.executionMode).toBe('manual');
|
||||
expect(result).toEqual({ executionId });
|
||||
});
|
||||
|
||||
test('should pass workflowIsActive to testWebhooks.needsWebhook', async () => {
|
||||
const userId = 'user-id';
|
||||
const user = mock<User>({ id: userId });
|
||||
const testWebhooks = mock<TestWebhooks>();
|
||||
const workflowRepositoryMock = mock<WorkflowRepository>();
|
||||
const telegramTrigger: INode = {
|
||||
id: '1',
|
||||
typeVersion: 1,
|
||||
position: [1, 2],
|
||||
parameters: {},
|
||||
name: 'Telegram Trigger',
|
||||
type: 'n8n-nodes-base.telegramTrigger',
|
||||
};
|
||||
const activeWorkflowData = {
|
||||
id: 'workflow-id',
|
||||
name: 'Test Workflow',
|
||||
active: true,
|
||||
activeVersionId: 'version-123',
|
||||
isArchived: false,
|
||||
nodes: [telegramTrigger],
|
||||
connections: {},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
workflowRepositoryMock.isActive.mockResolvedValue(true);
|
||||
const service = new WorkflowExecutionService(
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
workflowRepositoryMock,
|
||||
nodeTypes,
|
||||
testWebhooks,
|
||||
workflowRunner,
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
);
|
||||
|
||||
const runPayload: WorkflowRequest.FullManualExecutionFromKnownTriggerPayload = {
|
||||
workflowData: activeWorkflowData,
|
||||
triggerToStartFrom: { name: telegramTrigger.name },
|
||||
};
|
||||
|
||||
testWebhooks.needsWebhook.mockRejectedValue(
|
||||
new Error(
|
||||
'Cannot test webhook for node "Telegram Trigger" while workflow is active. Please deactivate the workflow first.',
|
||||
),
|
||||
);
|
||||
|
||||
await expect(service.executeManually(runPayload, user)).rejects.toThrow(
|
||||
'Cannot test webhook for node "Telegram Trigger" while workflow is active. Please deactivate the workflow first.',
|
||||
);
|
||||
|
||||
expect(testWebhooks.needsWebhook).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workflowIsActive: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectPinnedTrigger()', () => {
|
||||
|
||||
@@ -101,6 +101,9 @@ export class WorkflowExecutionService {
|
||||
user: User,
|
||||
pushRef?: string,
|
||||
): Promise<{ executionId: string } | { waitingForWebhook: boolean }> {
|
||||
// Check whether this workflow is active.
|
||||
const workflowIsActive = await this.workflowRepository.isActive(payload.workflowData.id);
|
||||
|
||||
// For manual testing always set to not active
|
||||
payload.workflowData.active = false;
|
||||
payload.workflowData.activeVersionId = null;
|
||||
@@ -146,6 +149,7 @@ export class WorkflowExecutionService {
|
||||
pushRef,
|
||||
triggerToStartFrom: payload.triggerToStartFrom,
|
||||
destinationNode: payload.destinationNode,
|
||||
workflowIsActive,
|
||||
}))
|
||||
) {
|
||||
return { waitingForWebhook: true };
|
||||
@@ -182,6 +186,7 @@ export class WorkflowExecutionService {
|
||||
}),
|
||||
pushRef,
|
||||
destinationNode: payload.destinationNode,
|
||||
workflowIsActive,
|
||||
}))
|
||||
) {
|
||||
return { waitingForWebhook: true };
|
||||
|
||||
@@ -123,24 +123,6 @@ describe('init()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isActive()', () => {
|
||||
it('should return `true` for active workflow in storage', async () => {
|
||||
const dbWorkflow = await createActiveWorkflow();
|
||||
|
||||
await activeWorkflowManager.init();
|
||||
|
||||
await expect(activeWorkflowManager.isActive(dbWorkflow.id)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('should return `false` for inactive workflow in storage', async () => {
|
||||
const dbWorkflow = await createInactiveWorkflow();
|
||||
|
||||
await activeWorkflowManager.init();
|
||||
|
||||
await expect(activeWorkflowManager.isActive(dbWorkflow.id)).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add()', () => {
|
||||
describe('in single-main mode', () => {
|
||||
test.each(['activate', 'update'])(
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
createWorkflow,
|
||||
testDb,
|
||||
getWorkflowById,
|
||||
setActiveVersion,
|
||||
} from '@n8n/backend-test-utils';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { WorkflowRepository, WorkflowDependencyRepository, WorkflowDependencies } from '@n8n/db';
|
||||
@@ -262,6 +263,25 @@ describe('WorkflowRepository', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isActive()', () => {
|
||||
it('should return `true` for active workflow in storage', async () => {
|
||||
const workflowRepository = Container.get(WorkflowRepository);
|
||||
|
||||
const workflow = await createWorkflowWithHistory();
|
||||
await setActiveVersion(workflow.id, workflow.versionId);
|
||||
|
||||
await expect(workflowRepository.isActive(workflow.id)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('should return `false` for inactive workflow in storage', async () => {
|
||||
const workflowRepository = Container.get(WorkflowRepository);
|
||||
|
||||
const workflow = await createWorkflowWithHistory();
|
||||
|
||||
await expect(workflowRepository.isActive(workflow.id)).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// NOTE: these tests use the workflow dependency repository, which is not enabled
|
||||
// on legacy Sqlite.
|
||||
const globalConfig = Container.get(GlobalConfig);
|
||||
|
||||
@@ -9,7 +9,12 @@ import {
|
||||
} from '@n8n/backend-test-utils';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import type { Project, TagEntity, User, WorkflowHistory } from '@n8n/db';
|
||||
import { ProjectRepository, WorkflowHistoryRepository, SharedWorkflowRepository } from '@n8n/db';
|
||||
import {
|
||||
WorkflowRepository,
|
||||
ProjectRepository,
|
||||
WorkflowHistoryRepository,
|
||||
SharedWorkflowRepository,
|
||||
} from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Not } from '@n8n/typeorm';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
@@ -37,6 +42,7 @@ let memberPersonalProject: Project;
|
||||
let authOwnerAgent: SuperAgentTest;
|
||||
let authMemberAgent: SuperAgentTest;
|
||||
let activeWorkflowManager: ActiveWorkflowManager;
|
||||
let workflowRepository: WorkflowRepository;
|
||||
|
||||
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
|
||||
const license = testServer.license;
|
||||
@@ -61,6 +67,7 @@ beforeAll(async () => {
|
||||
await utils.initNodeTypes();
|
||||
|
||||
activeWorkflowManager = Container.get(ActiveWorkflowManager);
|
||||
workflowRepository = Container.get(WorkflowRepository);
|
||||
|
||||
await activeWorkflowManager.init();
|
||||
});
|
||||
@@ -898,7 +905,7 @@ describe('POST /workflows/:id/activate', () => {
|
||||
expect(sharedWorkflow?.workflow.activeVersionId).toBe(workflow.versionId);
|
||||
|
||||
// check whether the workflow is on the active workflow runner
|
||||
expect(await activeWorkflowManager.isActive(workflow.id)).toBe(true);
|
||||
expect(await workflowRepository.isActive(workflow.id)).toBe(true);
|
||||
});
|
||||
|
||||
test('should set activeVersionId when activating workflow', async () => {
|
||||
@@ -974,7 +981,7 @@ describe('POST /workflows/:id/activate', () => {
|
||||
expect(sharedWorkflow?.workflow.activeVersionId).toBe(workflow.versionId);
|
||||
|
||||
// check whether the workflow is on the active workflow runner
|
||||
expect(await activeWorkflowManager.isActive(workflow.id)).toBe(true);
|
||||
expect(await workflowRepository.isActive(workflow.id)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1039,7 +1046,7 @@ describe('POST /workflows/:id/deactivate', () => {
|
||||
// check whether the workflow is deactivated in the database
|
||||
expect(sharedWorkflow?.workflow.activeVersionId).toBeNull();
|
||||
|
||||
expect(await activeWorkflowManager.isActive(workflow.id)).toBe(false);
|
||||
expect(await workflowRepository.isActive(workflow.id)).toBe(false);
|
||||
});
|
||||
|
||||
test('should clear activeVersionId when deactivating workflow', async () => {
|
||||
@@ -1126,7 +1133,7 @@ describe('POST /workflows/:id/deactivate', () => {
|
||||
|
||||
expect(sharedWorkflow?.workflow.activeVersionId).toBeNull();
|
||||
|
||||
expect(await activeWorkflowManager.isActive(workflow.id)).toBe(false);
|
||||
expect(await workflowRepository.isActive(workflow.id)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3083,7 +3083,6 @@
|
||||
"workflowPreview.showError.previewError.title": "Preview error",
|
||||
"workflowRun.noActiveConnectionToTheServer": "Lost connection to the server",
|
||||
"workflowRun.showError.deactivate": "Deactivate workflow to execute",
|
||||
"workflowRun.showError.productionActive": "Because of limitations in {nodeName}, n8n can't listen for test executions at the same time as listening for production ones",
|
||||
"workflowRun.showError.title": "Problem running workflow",
|
||||
"workflowRun.showError.payloadTooLarge": "Please execute the whole workflow, rather than just the node. (Existing execution data is too large.)",
|
||||
"workflowRun.showError.resolveOutstandingIssues": "Please resolve outstanding issues before you activate it",
|
||||
|
||||
@@ -25,14 +25,11 @@ import type { WorkflowData } from '@n8n/rest-api-client/api/workflows';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useWorkflowHelpers } from '@/app/composables/useWorkflowHelpers';
|
||||
import { useToast } from './useToast';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { captor, mock } from 'vitest-mock-extended';
|
||||
import { usePushConnectionStore } from '@/app/stores/pushConnection.store';
|
||||
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
|
||||
import { waitFor } from '@testing-library/vue';
|
||||
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
|
||||
import { SLACK_TRIGGER_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE } from '@/app/constants';
|
||||
|
||||
vi.mock('@/app/stores/workflows.store', () => {
|
||||
const storeState: Partial<ReturnType<typeof useWorkflowsStore>> & {
|
||||
@@ -250,177 +247,6 @@ describe('useRunWorkflow({ router })', () => {
|
||||
});
|
||||
|
||||
describe('runWorkflow()', () => {
|
||||
it('should prevent execution and show error message when workflow is active with single webhook trigger', async () => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
setActivePinia(pinia);
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
|
||||
vi.mocked(workflowsStore).isWorkflowActive = true;
|
||||
|
||||
vi.mocked(useWorkflowHelpers()).getWorkflowDataToSave.mockResolvedValue({
|
||||
nodes: [
|
||||
{
|
||||
name: 'Slack',
|
||||
type: SLACK_TRIGGER_NODE_TYPE,
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
} as unknown as WorkflowData);
|
||||
|
||||
const result = await runWorkflow({});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(toast.showMessage).toHaveBeenCalledWith({
|
||||
title: i18n.baseText('workflowRun.showError.deactivate'),
|
||||
message: i18n.baseText('workflowRun.showError.productionActive', {
|
||||
interpolate: { nodeName: 'Webhook' },
|
||||
}),
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute the workflow if the single webhook trigger has pin data', async () => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
setActivePinia(pinia);
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
|
||||
vi.mocked(workflowsStore).isWorkflowActive = true;
|
||||
|
||||
vi.mocked(useWorkflowHelpers()).getWorkflowDataToSave.mockResolvedValue({
|
||||
nodes: [
|
||||
{
|
||||
name: 'Slack',
|
||||
type: SLACK_TRIGGER_NODE_TYPE,
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
pinData: {
|
||||
Slack: [{ json: { value: 'data2' } }],
|
||||
},
|
||||
} as unknown as WorkflowData);
|
||||
|
||||
const mockExecutionResponse = { executionId: '123' };
|
||||
|
||||
vi.mocked(uiStore).activeActions = [''];
|
||||
vi.mocked(workflowsStore).workflowObject = {
|
||||
name: 'Test Workflow',
|
||||
} as unknown as Workflow;
|
||||
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
|
||||
vi.mocked(workflowsStore).nodesIssuesExist = true;
|
||||
vi.mocked(workflowsStore).getWorkflowRunData = {
|
||||
NodeName: [],
|
||||
};
|
||||
|
||||
const result = await runWorkflow({});
|
||||
expect(result).toEqual(mockExecutionResponse);
|
||||
|
||||
expect(toast.showMessage).not.toHaveBeenCalledWith({
|
||||
title: i18n.baseText('workflowRun.showError.deactivate'),
|
||||
message: i18n.baseText('workflowRun.showError.productionActive', {
|
||||
interpolate: { nodeName: 'Webhook' },
|
||||
}),
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute the workflow if there is a single webhook trigger, but another trigger is chosen', async () => {
|
||||
// ARRANGE
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
setActivePinia(pinia);
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
const mockExecutionResponse = { executionId: '123' };
|
||||
const triggerNode = 'Manual';
|
||||
|
||||
vi.mocked(workflowsStore).isWorkflowActive = true;
|
||||
vi.mocked(useWorkflowHelpers()).getWorkflowDataToSave.mockResolvedValue({
|
||||
nodes: [
|
||||
{
|
||||
name: 'Slack',
|
||||
type: SLACK_TRIGGER_NODE_TYPE,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
name: triggerNode,
|
||||
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
} as unknown as WorkflowData);
|
||||
vi.mocked(uiStore).activeActions = [''];
|
||||
vi.mocked(workflowsStore).workflowObject = {
|
||||
name: 'Test Workflow',
|
||||
} as unknown as Workflow;
|
||||
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
|
||||
vi.mocked(workflowsStore).nodesIssuesExist = true;
|
||||
vi.mocked(workflowsStore).getWorkflowRunData = { NodeName: [] };
|
||||
|
||||
// ACT
|
||||
const result = await runWorkflow({ triggerNode });
|
||||
|
||||
// ASSERT
|
||||
expect(result).toEqual(mockExecutionResponse);
|
||||
expect(toast.showMessage).not.toHaveBeenCalledWith({
|
||||
title: i18n.baseText('workflowRun.showError.deactivate'),
|
||||
message: i18n.baseText('workflowRun.showError.productionActive', {
|
||||
interpolate: { nodeName: 'Webhook' },
|
||||
}),
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should prevent execution and show error message when workflow is active with multiple triggers and a single webhook trigger is chosen', async () => {
|
||||
// ARRANGE
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
setActivePinia(pinia);
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
const mockExecutionResponse = { executionId: '123' };
|
||||
const triggerNode = 'Slack';
|
||||
|
||||
vi.mocked(workflowsStore).isWorkflowActive = true;
|
||||
vi.mocked(useWorkflowHelpers()).getWorkflowDataToSave.mockResolvedValue({
|
||||
nodes: [
|
||||
{
|
||||
name: triggerNode,
|
||||
type: SLACK_TRIGGER_NODE_TYPE,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
name: 'Manual',
|
||||
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
} as unknown as WorkflowData);
|
||||
vi.mocked(uiStore).activeActions = [''];
|
||||
vi.mocked(workflowsStore).workflowObject = {
|
||||
name: 'Test Workflow',
|
||||
} as unknown as Workflow;
|
||||
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
|
||||
vi.mocked(workflowsStore).nodesIssuesExist = true;
|
||||
vi.mocked(workflowsStore).getWorkflowRunData = { NodeName: [] };
|
||||
|
||||
// ACT
|
||||
const result = await runWorkflow({ triggerNode });
|
||||
|
||||
// ASSERT
|
||||
expect(result).toBeUndefined();
|
||||
expect(toast.showMessage).toHaveBeenCalledWith({
|
||||
title: i18n.baseText('workflowRun.showError.deactivate'),
|
||||
message: i18n.baseText('workflowRun.showError.productionActive', {
|
||||
interpolate: { nodeName: 'Webhook' },
|
||||
}),
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined if UI action "workflowRunning" is active', async () => {
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
workflowState.setActiveExecutionId('123');
|
||||
|
||||
@@ -22,11 +22,7 @@ import { retry } from '@n8n/utils/retry';
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
|
||||
|
||||
import {
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
IN_PROGRESS_EXECUTION_ID,
|
||||
SINGLE_WEBHOOK_TRIGGERS,
|
||||
} from '@/app/constants';
|
||||
import { CHAT_TRIGGER_NODE_TYPE, IN_PROGRESS_EXECUTION_ID } from '@/app/constants';
|
||||
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
@@ -287,31 +283,6 @@ export function useRunWorkflow(useRunWorkflowOpts: {
|
||||
return true;
|
||||
});
|
||||
|
||||
const singleWebhookTrigger =
|
||||
options.triggerNode === undefined
|
||||
? // if there is no chosen trigger we check all triggers
|
||||
triggers.find((node) => SINGLE_WEBHOOK_TRIGGERS.includes(node.type))
|
||||
: // if there is a chosen trigger we check this one only
|
||||
workflowData.nodes.find(
|
||||
(node) =>
|
||||
node.name === options.triggerNode && SINGLE_WEBHOOK_TRIGGERS.includes(node.type),
|
||||
);
|
||||
|
||||
if (
|
||||
singleWebhookTrigger &&
|
||||
workflowsStore.isWorkflowActive &&
|
||||
!workflowData.pinData?.[singleWebhookTrigger.name]
|
||||
) {
|
||||
toast.showMessage({
|
||||
title: i18n.baseText('workflowRun.showError.deactivate'),
|
||||
message: i18n.baseText('workflowRun.showError.productionActive', {
|
||||
interpolate: { nodeName: singleWebhookTrigger.name },
|
||||
}),
|
||||
type: 'error',
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const startRunData: IStartRunData = {
|
||||
workflowData,
|
||||
runData: isPartialExecution
|
||||
|
||||
@@ -143,12 +143,6 @@ export const OPEN_URL_PANEL_TRIGGER_NODE_TYPES = [
|
||||
MCP_TRIGGER_NODE_TYPE,
|
||||
];
|
||||
|
||||
export const SINGLE_WEBHOOK_TRIGGERS = [
|
||||
TELEGRAM_TRIGGER_NODE_TYPE,
|
||||
SLACK_TRIGGER_NODE_TYPE,
|
||||
FACEBOOK_LEAD_ADS_TRIGGER_NODE_TYPE,
|
||||
];
|
||||
|
||||
export const LIST_LIKE_NODE_OPERATIONS = ['getAll', 'getMany', 'read', 'search'];
|
||||
|
||||
export const PRODUCTION_ONLY_TRIGGER_NODE_TYPES = [CHAT_TRIGGER_NODE_TYPE];
|
||||
|
||||
Reference in New Issue
Block a user