From efee25ddaaf072713d56d956d57fc388cb427811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 31 Jul 2024 11:53:26 +0200 Subject: [PATCH 1/4] test(core): Stop showing JWT warning during test runs (no-changelog) (#10255) --- packages/cli/src/config/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index f743e7961dba5..dedc803839292 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -96,9 +96,10 @@ config.validate({ }); const userManagement = config.get('userManagement'); if (userManagement.jwtRefreshTimeoutHours >= userManagement.jwtSessionDurationHours) { - console.warn( - 'N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS needs to smaller than N8N_USER_MANAGEMENT_JWT_DURATION_HOURS. Setting N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS to 0 for now.', - ); + if (!inTest) + console.warn( + 'N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS needs to smaller than N8N_USER_MANAGEMENT_JWT_DURATION_HOURS. Setting N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS to 0 for now.', + ); config.set('userManagement.jwtRefreshTimeoutHours', 0); } From 3e96b293329525c9d4b2fcef87b3803e458c8e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 31 Jul 2024 12:22:52 +0200 Subject: [PATCH 2/4] fix(core): Restore log event `n8n.workflow.failed` (#10253) --- .../cli/src/WorkflowExecuteAdditionalData.ts | 2 + packages/cli/src/WorkflowRunner.ts | 1 + .../audit-event-relay.service.test.ts | 69 ++++++++++++++++++- .../src/eventbus/audit-event-relay.service.ts | 23 ++++++- packages/cli/src/eventbus/event.types.ts | 3 +- .../executions/execution-recovery.service.ts | 1 + 6 files changed, 94 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 754cfea693c3d..98ad9acde2936 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -651,6 +651,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { executionId, success: runData.status === 'success', isManual: runData.mode === 'manual', + runData, }); }, async function (this: WorkflowHooks, fullRunData: IRun) { @@ -940,6 +941,7 @@ async function executeWorkflow( success: data.status === 'success', isManual: data.mode === 'manual', userId: additionalData.userId, + runData: data, }); // subworkflow either finished, or is in status waiting due to a wait node, both cases are considered successes here diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index f8faf55fbe49d..3318dd283cd60 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -173,6 +173,7 @@ export class WorkflowRunner { success: executionData?.status === 'success', isManual: data.executionMode === 'manual', userId: data.userId, + runData: executionData, }); if (this.externalHooks.exists('workflow.postExecute')) { try { diff --git a/packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts b/packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts index 80392206079b7..145bf2929f07a 100644 --- a/packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts +++ b/packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts @@ -2,12 +2,14 @@ import { mock } from 'jest-mock-extended'; import { AuditEventRelay } from '../audit-event-relay.service'; import type { MessageEventBus } from '../MessageEventBus/MessageEventBus'; import type { Event } from '../event.types'; -import type { EventService } from '../event.service'; +import { EventService } from '../event.service'; +import type { INode, IRun } from 'n8n-workflow'; describe('AuditorService', () => { const eventBus = mock(); - const eventService = mock(); + const eventService = new EventService(); const auditor = new AuditEventRelay(eventService, eventBus); + auditor.init(); afterEach(() => { jest.clearAllMocks(); @@ -80,4 +82,67 @@ describe('AuditorService', () => { }, }); }); + + it('should log on `workflow-post-execute` for successful execution', () => { + const payload = mock({ + executionId: 'some-id', + success: true, + userId: 'some-id', + workflowId: 'some-id', + isManual: true, + workflowName: 'some-name', + metadata: {}, + runData: mock({ data: { resultData: {} } }), + }); + + eventService.emit('workflow-post-execute', payload); + + const { runData: _, ...rest } = payload; + + expect(eventBus.sendWorkflowEvent).toHaveBeenCalledWith({ + eventName: 'n8n.workflow.success', + payload: rest, + }); + }); + + it('should handle `workflow-post-execute` event for unsuccessful execution', () => { + const runData = mock({ + data: { + resultData: { + lastNodeExecuted: 'some-node', + // @ts-expect-error Partial mock + error: { + node: mock({ type: 'some-type' }), + message: 'some-message', + }, + errorMessage: 'some-message', + }, + }, + }) as unknown as IRun; + + const event = { + executionId: 'some-id', + success: false, + userId: 'some-id', + workflowId: 'some-id', + isManual: true, + workflowName: 'some-name', + metadata: {}, + runData, + }; + + eventService.emit('workflow-post-execute', event); + + const { runData: _, ...rest } = event; + + expect(eventBus.sendWorkflowEvent).toHaveBeenCalledWith({ + eventName: 'n8n.workflow.failed', + payload: { + ...rest, + lastNodeExecuted: 'some-node', + errorNodeType: 'some-type', + errorMessage: 'some-message', + }, + }); + }); }); diff --git a/packages/cli/src/eventbus/audit-event-relay.service.ts b/packages/cli/src/eventbus/audit-event-relay.service.ts index 9c73494520dbe..56f4dd95cd1e6 100644 --- a/packages/cli/src/eventbus/audit-event-relay.service.ts +++ b/packages/cli/src/eventbus/audit-event-relay.service.ts @@ -122,9 +122,28 @@ export class AuditEventRelay { } private workflowPostExecute(event: Event['workflow-post-execute']) { + const { runData, ...rest } = event; + + if (event.success) { + void this.eventBus.sendWorkflowEvent({ + eventName: 'n8n.workflow.success', + payload: rest, + }); + + return; + } + void this.eventBus.sendWorkflowEvent({ - eventName: 'n8n.workflow.success', - payload: event, + eventName: 'n8n.workflow.failed', + payload: { + ...rest, + lastNodeExecuted: runData?.data.resultData.lastNodeExecuted, + errorNodeType: + runData?.data.resultData.error && 'node' in runData?.data.resultData.error + ? runData?.data.resultData.error.node?.type + : undefined, + errorMessage: runData?.data.resultData.error?.message.toString(), + }, }); } diff --git a/packages/cli/src/eventbus/event.types.ts b/packages/cli/src/eventbus/event.types.ts index 225f9aca8c873..513c659d3e2a3 100644 --- a/packages/cli/src/eventbus/event.types.ts +++ b/packages/cli/src/eventbus/event.types.ts @@ -1,4 +1,4 @@ -import type { AuthenticationMethod, IWorkflowBase } from 'n8n-workflow'; +import type { AuthenticationMethod, IRun, IWorkflowBase } from 'n8n-workflow'; import type { IWorkflowExecutionDataProcess } from '@/Interfaces'; import type { ProjectRole } from '@/databases/entities/ProjectRelation'; import type { GlobalRole } from '@/databases/entities/User'; @@ -46,6 +46,7 @@ export type Event = { isManual: boolean; workflowName: string; metadata?: Record; + runData?: IRun; }; 'node-pre-execute': { diff --git a/packages/cli/src/executions/execution-recovery.service.ts b/packages/cli/src/executions/execution-recovery.service.ts index 05431a6d9c9d5..b72fc490dddbb 100644 --- a/packages/cli/src/executions/execution-recovery.service.ts +++ b/packages/cli/src/executions/execution-recovery.service.ts @@ -296,6 +296,7 @@ export class ExecutionRecoveryService { executionId: execution.id, success: execution.status === 'success', isManual: execution.mode === 'manual', + runData: execution, }); const externalHooks = getWorkflowHooksMain( From 8e960db16c28ad2b137120c3bf50b1c20eb1036b Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 31 Jul 2024 07:11:24 -0400 Subject: [PATCH 3/4] refactor(editor): Migrate `WorkflowExecutionsListView.vue` to composition API (no-changelog) (#10198) --- .../workflow/WorkflowExecutionsList.vue | 253 ++++++------------ .../src/views/WorkflowExecutionsView.vue | 4 - 2 files changed, 87 insertions(+), 170 deletions(-) diff --git a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsList.vue b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsList.vue index 22dd7c00e0fad..9e66df8117baa 100644 --- a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsList.vue +++ b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsList.vue @@ -5,10 +5,10 @@ :loading="loading && !executions.length" :loading-more="loadingMore" :temporary-execution="temporaryExecution" - @update:auto-refresh="$emit('update:auto-refresh', $event)" - @reload-executions="$emit('reload')" - @filter-updated="$emit('update:filters', $event)" - @load-more="$emit('load-more')" + @update:auto-refresh="emit('update:auto-refresh', $event)" + @reload-executions="emit('reload')" + @filter-updated="emit('update:filters', $event)" + @load-more="emit('load-more')" @retry-execution="onRetryExecution" />
@@ -23,177 +23,98 @@
- diff --git a/packages/editor-ui/src/views/WorkflowExecutionsView.vue b/packages/editor-ui/src/views/WorkflowExecutionsView.vue index 3cca54466e02c..86a19f603ba20 100644 --- a/packages/editor-ui/src/views/WorkflowExecutionsView.vue +++ b/packages/editor-ui/src/views/WorkflowExecutionsView.vue @@ -12,7 +12,6 @@ import { PLACEHOLDER_EMPTY_WORKFLOW_ID, VIEWS } from '@/constants'; import { useRoute, useRouter } from 'vue-router'; import type { ExecutionSummary } from 'n8n-workflow'; import { useDebounce } from '@/composables/useDebounce'; -import { storeToRefs } from 'pinia'; import { useTelemetry } from '@/composables/useTelemetry'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; @@ -29,8 +28,6 @@ const { callDebounced } = useDebounce(); const workflowHelpers = useWorkflowHelpers({ router }); const nodeHelpers = useNodeHelpers(); -const { filters } = storeToRefs(executionsStore); - const loading = ref(false); const loadingMore = ref(false); @@ -311,7 +308,6 @@ async function loadMore(): Promise { v-if="workflow" :executions="executions" :execution="execution" - :filters="filters" :workflow="workflow" :loading="loading" :loading-more="loadingMore" From cf73e29b6151fdc390e9e06036654d6b69d91016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 31 Jul 2024 13:40:01 +0200 Subject: [PATCH 4/4] test(core): Improve and expand log streaming tests (no-changelog) (#10261) --- .../audit-event-relay.service.test.ts | 724 +++++++++++++++--- 1 file changed, 617 insertions(+), 107 deletions(-) diff --git a/packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts b/packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts index 145bf2929f07a..7196b9203c407 100644 --- a/packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts +++ b/packages/cli/src/eventbus/__tests__/audit-event-relay.service.test.ts @@ -3,9 +3,9 @@ import { AuditEventRelay } from '../audit-event-relay.service'; import type { MessageEventBus } from '../MessageEventBus/MessageEventBus'; import type { Event } from '../event.types'; import { EventService } from '../event.service'; -import type { INode, IRun } from 'n8n-workflow'; +import type { INode, IRun, IWorkflowBase } from 'n8n-workflow'; -describe('AuditorService', () => { +describe('AuditEventRelay', () => { const eventBus = mock(); const eventService = new EventService(); const auditor = new AuditEventRelay(eventService, eventBus); @@ -15,134 +15,644 @@ describe('AuditorService', () => { jest.clearAllMocks(); }); - it('should handle `user-deleted` event', () => { - const arg: Event['user-deleted'] = { - user: { - id: '123', - email: 'john@n8n.io', - firstName: 'John', - lastName: 'Doe', - role: 'some-role', - }, - }; - - // @ts-expect-error Private method - auditor.userDeleted(arg); - - expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ - eventName: 'n8n.audit.user.deleted', - payload: { - userId: '123', - _email: 'john@n8n.io', - _firstName: 'John', - _lastName: 'Doe', - globalRole: 'some-role', - }, - }); - }); + describe('workflow events', () => { + it('should log on `workflow-created` event', () => { + const event: Event['workflow-created'] = { + user: { + id: '123', + email: 'john@n8n.io', + firstName: 'John', + lastName: 'Doe', + role: 'owner', + }, + workflow: mock({ + id: 'wf123', + name: 'Test Workflow', + }), + }; - it('should handle `user-invite-email-click` event', () => { - const arg: Event['user-invite-email-click'] = { - inviter: { - id: '123', - email: 'john@n8n.io', - firstName: 'John', - lastName: 'Doe', - role: 'some-role', - }, - invitee: { - id: '456', - email: 'jane@n8n.io', - firstName: 'Jane', - lastName: 'Doe', - role: 'some-other-role', - }, - }; - - // @ts-expect-error Private method - auditor.userInviteEmailClick(arg); - - expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ - eventName: 'n8n.audit.user.invitation.accepted', - payload: { - inviter: { + eventService.emit('workflow-created', event); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.workflow.created', + payload: { userId: '123', _email: 'john@n8n.io', _firstName: 'John', _lastName: 'Doe', - globalRole: 'some-role', + globalRole: 'owner', + workflowId: 'wf123', + workflowName: 'Test Workflow', }, - invitee: { + }); + }); + + it('should log on `workflow-deleted` event', () => { + const event: Event['workflow-deleted'] = { + user: { + id: '456', + email: 'jane@n8n.io', + firstName: 'Jane', + lastName: 'Smith', + role: 'user', + }, + workflowId: 'wf789', + }; + + eventService.emit('workflow-deleted', event); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.workflow.deleted', + payload: { userId: '456', _email: 'jane@n8n.io', _firstName: 'Jane', + _lastName: 'Smith', + globalRole: 'user', + workflowId: 'wf789', + }, + }); + }); + + it('should log on `workflow-saved` event', () => { + const event: Event['workflow-saved'] = { + user: { + id: '789', + email: 'alex@n8n.io', + firstName: 'Alex', + lastName: 'Johnson', + role: 'editor', + }, + workflowId: 'wf101', + workflowName: 'Updated Workflow', + }; + + eventService.emit('workflow-saved', event); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.workflow.updated', + payload: { + userId: '789', + _email: 'alex@n8n.io', + _firstName: 'Alex', + _lastName: 'Johnson', + globalRole: 'editor', + workflowId: 'wf101', + workflowName: 'Updated Workflow', + }, + }); + }); + + it('should log on `workflow-pre-execute` event', () => { + const workflow = mock({ + id: 'wf202', + name: 'Test Workflow', + active: true, + nodes: [], + connections: {}, + staticData: undefined, + settings: {}, + }); + + const event: Event['workflow-pre-execute'] = { + executionId: 'exec123', + data: workflow, + }; + + eventService.emit('workflow-pre-execute', event); + + expect(eventBus.sendWorkflowEvent).toHaveBeenCalledWith({ + eventName: 'n8n.workflow.started', + payload: { + executionId: 'exec123', + userId: undefined, + workflowId: 'wf202', + isManual: false, + workflowName: 'Test Workflow', + }, + }); + }); + + it('should log on `workflow-post-execute` for successful execution', () => { + const payload = mock({ + executionId: 'some-id', + success: true, + userId: 'some-id', + workflowId: 'some-id', + isManual: true, + workflowName: 'some-name', + metadata: {}, + runData: mock({ data: { resultData: {} } }), + }); + + eventService.emit('workflow-post-execute', payload); + + const { runData: _, ...rest } = payload; + + expect(eventBus.sendWorkflowEvent).toHaveBeenCalledWith({ + eventName: 'n8n.workflow.success', + payload: rest, + }); + }); + + it('should handle `workflow-post-execute` event for unsuccessful execution', () => { + const runData = mock({ + data: { + resultData: { + lastNodeExecuted: 'some-node', + // @ts-expect-error Partial mock + error: { + node: mock({ type: 'some-type' }), + message: 'some-message', + }, + errorMessage: 'some-message', + }, + }, + }) as unknown as IRun; + + const event = { + executionId: 'some-id', + success: false, + userId: 'some-id', + workflowId: 'some-id', + isManual: true, + workflowName: 'some-name', + metadata: {}, + runData, + }; + + eventService.emit('workflow-post-execute', event); + + const { runData: _, ...rest } = event; + + expect(eventBus.sendWorkflowEvent).toHaveBeenCalledWith({ + eventName: 'n8n.workflow.failed', + payload: { + ...rest, + lastNodeExecuted: 'some-node', + errorNodeType: 'some-type', + errorMessage: 'some-message', + }, + }); + }); + }); + + describe('user events', () => { + it('should log on `user-updated` event', () => { + const event: Event['user-updated'] = { + user: { + id: 'user456', + email: 'updated@example.com', + firstName: 'Updated', + lastName: 'User', + role: 'global:member', + }, + fieldsChanged: ['firstName', 'lastName', 'password'], + }; + + eventService.emit('user-updated', event); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.user.updated', + payload: { + userId: 'user456', + _email: 'updated@example.com', + _firstName: 'Updated', + _lastName: 'User', + globalRole: 'global:member', + fieldsChanged: ['firstName', 'lastName', 'password'], + }, + }); + }); + + it('should log on `user-deleted` event', () => { + const event: Event['user-deleted'] = { + user: { + id: '123', + email: 'john@n8n.io', + firstName: 'John', + lastName: 'Doe', + role: 'some-role', + }, + }; + + eventService.emit('user-deleted', event); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.user.deleted', + payload: { + userId: '123', + _email: 'john@n8n.io', + _firstName: 'John', _lastName: 'Doe', - globalRole: 'some-other-role', + globalRole: 'some-role', }, - }, + }); }); }); - it('should log on `workflow-post-execute` for successful execution', () => { - const payload = mock({ - executionId: 'some-id', - success: true, - userId: 'some-id', - workflowId: 'some-id', - isManual: true, - workflowName: 'some-name', - metadata: {}, - runData: mock({ data: { resultData: {} } }), + describe('click events', () => { + it('should log on `user-password-reset-request-click` event', () => { + const event: Event['user-password-reset-request-click'] = { + user: { + id: 'user101', + email: 'user101@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:member', + }, + }; + + eventService.emit('user-password-reset-request-click', event); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.user.reset.requested', + payload: { + userId: 'user101', + _email: 'user101@example.com', + _firstName: 'John', + _lastName: 'Doe', + globalRole: 'global:member', + }, + }); }); - eventService.emit('workflow-post-execute', payload); + it('should log on `user-invite-email-click` event', () => { + const event: Event['user-invite-email-click'] = { + inviter: { + id: '123', + email: 'john@n8n.io', + firstName: 'John', + lastName: 'Doe', + role: 'some-role', + }, + invitee: { + id: '456', + email: 'jane@n8n.io', + firstName: 'Jane', + lastName: 'Doe', + role: 'some-other-role', + }, + }; - const { runData: _, ...rest } = payload; + eventService.emit('user-invite-email-click', event); - expect(eventBus.sendWorkflowEvent).toHaveBeenCalledWith({ - eventName: 'n8n.workflow.success', - payload: rest, + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.user.invitation.accepted', + payload: { + inviter: { + userId: '123', + _email: 'john@n8n.io', + _firstName: 'John', + _lastName: 'Doe', + globalRole: 'some-role', + }, + invitee: { + userId: '456', + _email: 'jane@n8n.io', + _firstName: 'Jane', + _lastName: 'Doe', + globalRole: 'some-other-role', + }, + }, + }); }); }); - it('should handle `workflow-post-execute` event for unsuccessful execution', () => { - const runData = mock({ - data: { - resultData: { - lastNodeExecuted: 'some-node', - // @ts-expect-error Partial mock - error: { - node: mock({ type: 'some-type' }), - message: 'some-message', + describe('node events', () => { + it('should log on `node-pre-execute` event', () => { + const workflow = mock({ + id: 'wf303', + name: 'Test Workflow with Nodes', + active: true, + nodes: [ + { + id: 'node1', + name: 'Start Node', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [100, 200], }, - errorMessage: 'some-message', + { + id: 'node2', + name: 'HTTP Request', + type: 'n8n-nodes-base.httpRequest', + typeVersion: 1, + position: [300, 200], + }, + ], + connections: {}, + settings: {}, + }); + + const event: Event['node-pre-execute'] = { + executionId: 'exec456', + nodeName: 'HTTP Request', + workflow, + }; + + eventService.emit('node-pre-execute', event); + + expect(eventBus.sendNodeEvent).toHaveBeenCalledWith({ + eventName: 'n8n.node.started', + payload: { + executionId: 'exec456', + nodeName: 'HTTP Request', + workflowId: 'wf303', + workflowName: 'Test Workflow with Nodes', + nodeType: 'n8n-nodes-base.httpRequest', + }, + }); + }); + + it('should log on `node-post-execute` event', () => { + const workflow = mock({ + id: 'wf404', + name: 'Test Workflow with Completed Node', + active: true, + nodes: [ + { + id: 'node1', + name: 'Start Node', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [100, 200], + }, + { + id: 'node2', + name: 'HTTP Response', + type: 'n8n-nodes-base.httpResponse', + typeVersion: 1, + position: [300, 200], + }, + ], + connections: {}, + settings: {}, + }); + + const event: Event['node-post-execute'] = { + executionId: 'exec789', + nodeName: 'HTTP Response', + workflow, + }; + + eventService.emit('node-post-execute', event); + + expect(eventBus.sendNodeEvent).toHaveBeenCalledWith({ + eventName: 'n8n.node.finished', + payload: { + executionId: 'exec789', + nodeName: 'HTTP Response', + workflowId: 'wf404', + workflowName: 'Test Workflow with Completed Node', + nodeType: 'n8n-nodes-base.httpResponse', + }, + }); + }); + }); + + describe('credentials events', () => { + it('should log on `credentials-shared` event', () => { + const event: Event['credentials-shared'] = { + user: { + id: 'user123', + email: 'sharer@example.com', + firstName: 'Alice', + lastName: 'Sharer', + role: 'global:owner', + }, + credentialId: 'cred789', + credentialType: 'githubApi', + userIdSharer: 'user123', + userIdsShareesAdded: ['user456', 'user789'], + shareesRemoved: null, + }; + + eventService.emit('credentials-shared', event); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.user.credentials.shared', + payload: { + userId: 'user123', + _email: 'sharer@example.com', + _firstName: 'Alice', + _lastName: 'Sharer', + globalRole: 'global:owner', + credentialId: 'cred789', + credentialType: 'githubApi', + userIdSharer: 'user123', + userIdsShareesAdded: ['user456', 'user789'], + shareesRemoved: null, + }, + }); + }); + + it('should log on `credentials-created` event', () => { + const event: Event['credentials-created'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'Test', + lastName: 'User', + role: 'global:owner', + }, + credentialType: 'githubApi', + credentialId: 'cred456', + publicApi: false, + projectId: 'proj789', + projectType: 'Personal', + }; + + eventService.emit('credentials-created', event); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.user.credentials.created', + payload: { + userId: 'user123', + _email: 'user@example.com', + _firstName: 'Test', + _lastName: 'User', + globalRole: 'global:owner', + credentialType: 'githubApi', + credentialId: 'cred456', + publicApi: false, + projectId: 'proj789', + projectType: 'Personal', + }, + }); + }); + }); + + describe('auth events', () => { + it('should log on `user-login-failed` event', () => { + const event: Event['user-login-failed'] = { + userEmail: 'user@example.com', + authenticationMethod: 'email', + reason: 'Invalid password', + }; + + eventService.emit('user-login-failed', event); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.user.login.failed', + payload: { + userEmail: 'user@example.com', + authenticationMethod: 'email', + reason: 'Invalid password', + }, + }); + }); + }); + + describe('community package events', () => { + it('should log on `community-package-updated` event', () => { + const event: Event['community-package-updated'] = { + user: { + id: 'user202', + email: 'packageupdater@example.com', + firstName: 'Package', + lastName: 'Updater', + role: 'global:admin', + }, + packageName: 'n8n-nodes-awesome-package', + packageVersionCurrent: '1.0.0', + packageVersionNew: '1.1.0', + packageNodeNames: ['AwesomeNode1', 'AwesomeNode2'], + packageAuthor: 'Jane Doe', + packageAuthorEmail: 'jane@example.com', + }; + + eventService.emit('community-package-updated', event); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.package.updated', + payload: { + userId: 'user202', + _email: 'packageupdater@example.com', + _firstName: 'Package', + _lastName: 'Updater', + globalRole: 'global:admin', + packageName: 'n8n-nodes-awesome-package', + packageVersionCurrent: '1.0.0', + packageVersionNew: '1.1.0', + packageNodeNames: ['AwesomeNode1', 'AwesomeNode2'], + packageAuthor: 'Jane Doe', + packageAuthorEmail: 'jane@example.com', + }, + }); + }); + + it('should log on `community-package-installed` event', () => { + const event: Event['community-package-installed'] = { + user: { + id: 'user789', + email: 'admin@example.com', + firstName: 'Admin', + lastName: 'User', + role: 'global:admin', + }, + inputString: 'n8n-nodes-custom-package', + packageName: 'n8n-nodes-custom-package', + success: true, + packageVersion: '1.0.0', + packageNodeNames: ['CustomNode1', 'CustomNode2'], + packageAuthor: 'John Doe', + packageAuthorEmail: 'john@example.com', + }; + + eventService.emit('community-package-installed', event); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.package.installed', + payload: { + userId: 'user789', + _email: 'admin@example.com', + _firstName: 'Admin', + _lastName: 'User', + globalRole: 'global:admin', + inputString: 'n8n-nodes-custom-package', + packageName: 'n8n-nodes-custom-package', + success: true, + packageVersion: '1.0.0', + packageNodeNames: ['CustomNode1', 'CustomNode2'], + packageAuthor: 'John Doe', + packageAuthorEmail: 'john@example.com', + }, + }); + }); + }); + + describe('email events', () => { + it('should log on `email-failed` event', () => { + const event: Event['email-failed'] = { + user: { + id: 'user789', + email: 'recipient@example.com', + firstName: 'Failed', + lastName: 'Recipient', + role: 'global:member', + }, + messageType: 'New user invite', + }; + + eventService.emit('email-failed', event); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.user.email.failed', + payload: { + userId: 'user789', + _email: 'recipient@example.com', + _firstName: 'Failed', + _lastName: 'Recipient', + globalRole: 'global:member', + messageType: 'New user invite', + }, + }); + }); + }); + + describe('public API events', () => { + it('should log on `public-api-key-created` event', () => { + const event: Event['public-api-key-created'] = { + user: { + id: 'user101', + email: 'apiuser@example.com', + firstName: 'API', + lastName: 'User', + role: 'global:owner', + }, + publicApi: true, + }; + + eventService.emit('public-api-key-created', event); + + expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({ + eventName: 'n8n.audit.user.api.created', + payload: { + userId: 'user101', + _email: 'apiuser@example.com', + _firstName: 'API', + _lastName: 'User', + globalRole: 'global:owner', + }, + }); + }); + }); + + describe('execution events', () => { + it('should log on `execution-throttled` event', () => { + const event: Event['execution-throttled'] = { + executionId: 'exec123456', + }; + + eventService.emit('execution-throttled', event); + + expect(eventBus.sendExecutionEvent).toHaveBeenCalledWith({ + eventName: 'n8n.execution.throttled', + payload: { + executionId: 'exec123456', }, - }, - }) as unknown as IRun; - - const event = { - executionId: 'some-id', - success: false, - userId: 'some-id', - workflowId: 'some-id', - isManual: true, - workflowName: 'some-name', - metadata: {}, - runData, - }; - - eventService.emit('workflow-post-execute', event); - - const { runData: _, ...rest } = event; - - expect(eventBus.sendWorkflowEvent).toHaveBeenCalledWith({ - eventName: 'n8n.workflow.failed', - payload: { - ...rest, - lastNodeExecuted: 'some-node', - errorNodeType: 'some-type', - errorMessage: 'some-message', - }, + }); }); }); });