diff --git a/packages/@n8n/db/src/repositories/workflow.repository.ts b/packages/@n8n/db/src/repositories/workflow.repository.ts index e428823ef53..6f5c4d0b361 100644 --- a/packages/@n8n/db/src/repositories/workflow.repository.ts +++ b/packages/@n8n/db/src/repositories/workflow.repository.ts @@ -981,4 +981,16 @@ export class WorkflowRepository extends Repository { 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; + } } diff --git a/packages/cli/src/active-workflow-manager.ts b/packages/cli/src/active-workflow-manager.ts index 6c8372baa67..ff02ae145bf 100644 --- a/packages/cli/src/active-workflow-manager.ts +++ b/packages/cli/src/active-workflow-manager.ts @@ -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. */ diff --git a/packages/cli/src/errors/single-webhook-trigger.error.ts b/packages/cli/src/errors/single-webhook-trigger.error.ts new file mode 100644 index 00000000000..2627960863b --- /dev/null +++ b/packages/cli/src/errors/single-webhook-trigger.error.ts @@ -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 } }, + ); + } +} diff --git a/packages/cli/src/webhooks/__tests__/test-webhooks.test.ts b/packages/cli/src/webhooks/__tests__/test-webhooks.test.ts index aa4a7e8bea3..d2b00b2cb14 100644 --- a/packages/cli/src/webhooks/__tests__/test-webhooks.test.ts +++ b/packages/cli/src/webhooks/__tests__/test-webhooks.test.ts @@ -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(); + const regularWebhook = mock({ + node: 'Webhook', + httpMethod, + path: 'regular-path', + workflowId: workflowEntity.id, + userId, + }); + const telegramWebhook = mock({ + node: 'Telegram Trigger', + httpMethod, + path: 'telegram-path', + workflowId: workflowEntity.id, + userId, + }); + const webhookNode = mock({ + name: 'Webhook', + type: 'n8n-nodes-base.webhook', + }); + const telegramNode = mock({ + 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()', () => { diff --git a/packages/cli/src/webhooks/test-webhooks.ts b/packages/cli/src/webhooks/test-webhooks.ts index 466c2465152..d58808dfe1d 100644 --- a/packages/cli/src/webhooks/test-webhooks.ts +++ b/packages/cli/src/webhooks/test-webhooks.ts @@ -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, diff --git a/packages/cli/src/workflows/__tests__/workflow-execution.service.test.ts b/packages/cli/src/workflows/__tests__/workflow-execution.service.test.ts index 06ce635bb31..92c4b83c061 100644 --- a/packages/cli/src/workflows/__tests__/workflow-execution.service.test.ts +++ b/packages/cli/src/workflows/__tests__/workflow-execution.service.test.ts @@ -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({ id: userId }); + const testWebhooks = mock(); + const workflowRepositoryMock = mock(); + 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()', () => { diff --git a/packages/cli/src/workflows/workflow-execution.service.ts b/packages/cli/src/workflows/workflow-execution.service.ts index 25ae95dfd37..d58e89a9e3b 100644 --- a/packages/cli/src/workflows/workflow-execution.service.ts +++ b/packages/cli/src/workflows/workflow-execution.service.ts @@ -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 }; diff --git a/packages/cli/test/integration/active-workflow-manager.test.ts b/packages/cli/test/integration/active-workflow-manager.test.ts index f65b7c73639..0e0c94a4754 100644 --- a/packages/cli/test/integration/active-workflow-manager.test.ts +++ b/packages/cli/test/integration/active-workflow-manager.test.ts @@ -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'])( diff --git a/packages/cli/test/integration/database/repositories/workflow.repository.test.ts b/packages/cli/test/integration/database/repositories/workflow.repository.test.ts index 9f6f4594afd..df52156cfe2 100644 --- a/packages/cli/test/integration/database/repositories/workflow.repository.test.ts +++ b/packages/cli/test/integration/database/repositories/workflow.repository.test.ts @@ -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); diff --git a/packages/cli/test/integration/public-api/workflows.test.ts b/packages/cli/test/integration/public-api/workflows.test.ts index 97b24da0762..67bad030ddc 100644 --- a/packages/cli/test/integration/public-api/workflows.test.ts +++ b/packages/cli/test/integration/public-api/workflows.test.ts @@ -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); }); }); diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index a30c8627ed5..d859015d0a6 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -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", diff --git a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts index 2f506a6a080..a7d06f45527 100644 --- a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts @@ -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> & { @@ -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'); diff --git a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts index 4bb64c7d08d..1fb80d8e1b5 100644 --- a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts +++ b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts @@ -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 diff --git a/packages/frontend/editor-ui/src/app/constants/nodeTypes.ts b/packages/frontend/editor-ui/src/app/constants/nodeTypes.ts index ae42317610f..2d1ee2118ee 100644 --- a/packages/frontend/editor-ui/src/app/constants/nodeTypes.ts +++ b/packages/frontend/editor-ui/src/app/constants/nodeTypes.ts @@ -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];