first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import { GLOBAL_OWNER_ROLE, type User } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import axios from 'axios';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type {
|
||||
MessageEventBusDestinationSentryOptions,
|
||||
MessageEventBusDestinationSyslogOptions,
|
||||
MessageEventBusDestinationWebhookOptions,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
defaultMessageEventBusDestinationSentryOptions,
|
||||
defaultMessageEventBusDestinationSyslogOptions,
|
||||
defaultMessageEventBusDestinationWebhookOptions,
|
||||
} from 'n8n-workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import type { EventNamesTypes } from '@/eventbus/event-message-classes';
|
||||
import { EventMessageAudit } from '@/eventbus/event-message-classes/event-message-audit';
|
||||
import { EventMessageGeneric } from '@/eventbus/event-message-classes/event-message-generic';
|
||||
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
||||
import { ExecutionRecoveryService } from '@/executions/execution-recovery.service';
|
||||
import type { MessageEventBusDestinationSentry } from '@/modules/log-streaming.ee/destinations/message-event-bus-destination-sentry.ee';
|
||||
import type { MessageEventBusDestinationSyslog } from '@/modules/log-streaming.ee/destinations/message-event-bus-destination-syslog.ee';
|
||||
import type { MessageEventBusDestinationWebhook } from '@/modules/log-streaming.ee/destinations/message-event-bus-destination-webhook.ee';
|
||||
import { LogStreamingDestinationService } from '@/modules/log-streaming.ee/log-streaming-destination.service';
|
||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
|
||||
import { createUser } from './shared/db/users';
|
||||
import type { SuperAgentTest } from './shared/types';
|
||||
import * as utils from './shared/utils';
|
||||
|
||||
jest.unmock('@/eventbus/message-event-bus/message-event-bus');
|
||||
jest.mock('axios');
|
||||
|
||||
const mockAxiosInstance = mock<ReturnType<typeof axios.create>>();
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
mockedAxios.create.mockReturnValue(mockAxiosInstance);
|
||||
|
||||
mockInstance(Publisher);
|
||||
|
||||
let owner: User;
|
||||
let authOwnerAgent: SuperAgentTest;
|
||||
|
||||
const testSyslogDestination: MessageEventBusDestinationSyslogOptions = {
|
||||
...defaultMessageEventBusDestinationSyslogOptions,
|
||||
id: 'b88038f4-0a89-4e94-89a9-658dfdb74539',
|
||||
protocol: 'udp',
|
||||
label: 'Test Syslog',
|
||||
enabled: false,
|
||||
subscribedEvents: ['n8n.test.message', 'n8n.audit.user.updated'],
|
||||
};
|
||||
|
||||
const testWebhookDestination: MessageEventBusDestinationWebhookOptions = {
|
||||
...defaultMessageEventBusDestinationWebhookOptions,
|
||||
id: '88be6560-bfb4-455c-8aa1-06971e9e5522',
|
||||
url: 'http://localhost:3456',
|
||||
method: 'POST',
|
||||
label: 'Test Webhook',
|
||||
enabled: false,
|
||||
subscribedEvents: ['n8n.test.message', 'n8n.audit.user.updated'],
|
||||
};
|
||||
|
||||
const testSentryDestination: MessageEventBusDestinationSentryOptions = {
|
||||
...defaultMessageEventBusDestinationSentryOptions,
|
||||
id: '450ca04b-87dd-4837-a052-ab3a347a00e9',
|
||||
dsn: 'http://localhost:3000',
|
||||
label: 'Test Sentry',
|
||||
enabled: false,
|
||||
subscribedEvents: ['n8n.test.message', 'n8n.audit.user.updated'],
|
||||
};
|
||||
|
||||
let eventBus: MessageEventBus;
|
||||
let destinationService: LogStreamingDestinationService;
|
||||
|
||||
async function confirmIdInAll(id: string) {
|
||||
const sent = await eventBus.getEventsAll();
|
||||
expect(sent.length).toBeGreaterThan(0);
|
||||
expect(sent.find((msg) => msg.id === id)).toBeTruthy();
|
||||
}
|
||||
|
||||
async function confirmIdSent(id: string) {
|
||||
const sent = await eventBus.getEventsSent();
|
||||
expect(sent.length).toBeGreaterThan(0);
|
||||
expect(sent.find((msg) => msg.id === id)).toBeTruthy();
|
||||
}
|
||||
|
||||
mockInstance(ExecutionRecoveryService);
|
||||
const testServer = utils.setupTestServer({
|
||||
endpointGroups: ['eventBus'],
|
||||
enabledFeatures: ['feat:logStreaming'],
|
||||
modules: ['log-streaming'],
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
owner = await createUser({ role: GLOBAL_OWNER_ROLE });
|
||||
authOwnerAgent = testServer.authAgentFor(owner);
|
||||
|
||||
eventBus = Container.get(MessageEventBus);
|
||||
await eventBus.initialize();
|
||||
|
||||
destinationService = Container.get(LogStreamingDestinationService);
|
||||
await destinationService.initialize();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
jest.mock('@/eventbus/message-event-bus/message-event-bus');
|
||||
await eventBus?.close();
|
||||
});
|
||||
|
||||
test('should have a running logwriter process', () => {
|
||||
const thread = eventBus.logWriter.worker;
|
||||
expect(thread).toBeDefined();
|
||||
});
|
||||
|
||||
test('should have logwriter log messages', async () => {
|
||||
const testMessage = new EventMessageGeneric({
|
||||
eventName: 'n8n.test.message' as EventNamesTypes,
|
||||
id: uuid(),
|
||||
});
|
||||
await eventBus.send(testMessage);
|
||||
await new Promise((resolve) => {
|
||||
eventBus.logWriter.worker?.once('message', async (msg: { command: string; data: any }) => {
|
||||
expect(msg.command).toBe('appendMessageToLog');
|
||||
expect(msg.data).toBe(true);
|
||||
await confirmIdInAll(testMessage.id);
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /eventbus/destination', () => {
|
||||
test('should fail due to missing authentication', async () => {
|
||||
const response = await testServer.authlessAgent.get('/eventbus/destination');
|
||||
expect(response.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('all returned destinations should exist in eventbus', async () => {
|
||||
const response = await authOwnerAgent.get('/eventbus/destination');
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const data = response.body.data;
|
||||
expect(data).toBeTruthy();
|
||||
expect(Array.isArray(data)).toBeTruthy();
|
||||
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
const destination = data[index];
|
||||
const foundDestinations = await destinationService.findDestination(destination.id);
|
||||
expect(Array.isArray(foundDestinations)).toBeTruthy();
|
||||
expect(foundDestinations.length).toBe(1);
|
||||
expect(foundDestinations[0].label).toBe(destination.label);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /eventbus/destination', () => {
|
||||
test('create syslog destination', async () => {
|
||||
const response = await authOwnerAgent.post('/eventbus/destination').send(testSyslogDestination);
|
||||
expect(response.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('create sentry destination', async () => {
|
||||
const response = await authOwnerAgent.post('/eventbus/destination').send(testSentryDestination);
|
||||
expect(response.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('create webhook destination', async () => {
|
||||
const response = await authOwnerAgent
|
||||
.post('/eventbus/destination')
|
||||
.send(testWebhookDestination);
|
||||
expect(response.statusCode).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
test('should anonymize audit message to syslog ', async () => {
|
||||
const testAuditMessage = new EventMessageAudit({
|
||||
eventName: 'n8n.audit.user.updated',
|
||||
payload: {
|
||||
_secret: 'secret',
|
||||
public: 'public',
|
||||
},
|
||||
id: uuid(),
|
||||
});
|
||||
|
||||
const syslogDestination = destinationService['destinations'][
|
||||
testSyslogDestination.id!
|
||||
] as MessageEventBusDestinationSyslog;
|
||||
|
||||
syslogDestination.enabled = true;
|
||||
|
||||
const mockedSyslogClientLog = jest.spyOn(syslogDestination.client, 'log');
|
||||
mockedSyslogClientLog.mockImplementation(async (m, _options, _cb) => {
|
||||
const o = JSON.parse(m);
|
||||
expect(o).toHaveProperty('payload');
|
||||
expect(o.payload).toHaveProperty('_secret');
|
||||
syslogDestination.anonymizeAuditMessages
|
||||
? expect(o.payload._secret).toBe('*')
|
||||
: expect(o.payload._secret).toBe('secret');
|
||||
expect(o.payload).toHaveProperty('public');
|
||||
expect(o.payload.public).toBe('public');
|
||||
});
|
||||
|
||||
syslogDestination.anonymizeAuditMessages = true;
|
||||
await eventBus.send(testAuditMessage);
|
||||
await new Promise((resolve) => {
|
||||
eventBus.logWriter.worker?.on(
|
||||
'message',
|
||||
async function handler005(msg: { command: string; data: any }) {
|
||||
if (msg.command === 'appendMessageToLog') {
|
||||
await eventBus.getEventsAll();
|
||||
await confirmIdInAll(testAuditMessage.id);
|
||||
expect(mockedSyslogClientLog).toHaveBeenCalled();
|
||||
eventBus.logWriter.worker?.removeListener('message', handler005);
|
||||
resolve(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
syslogDestination.anonymizeAuditMessages = false;
|
||||
await eventBus.send(testAuditMessage);
|
||||
await new Promise((resolve) => {
|
||||
eventBus.logWriter.worker?.on(
|
||||
'message',
|
||||
async function handler006(msg: { command: string; data: any }) {
|
||||
if (msg.command === 'appendMessageToLog') {
|
||||
await eventBus.getEventsAll();
|
||||
await confirmIdInAll(testAuditMessage.id);
|
||||
expect(mockedSyslogClientLog).toHaveBeenCalled();
|
||||
syslogDestination.enabled = false;
|
||||
eventBus.logWriter.worker?.removeListener('message', handler006);
|
||||
resolve(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should send message to webhook ', async () => {
|
||||
const testMessage = new EventMessageGeneric({
|
||||
eventName: 'n8n.test.message' as EventNamesTypes,
|
||||
id: uuid(),
|
||||
});
|
||||
|
||||
const webhookDestination = destinationService['destinations'][
|
||||
testWebhookDestination.id!
|
||||
] as MessageEventBusDestinationWebhook;
|
||||
|
||||
webhookDestination.enabled = true;
|
||||
|
||||
mockAxiosInstance.post.mockResolvedValue({ status: 200, data: { msg: 'OK' } });
|
||||
mockAxiosInstance.request.mockResolvedValue({ status: 200, data: { msg: 'OK' } });
|
||||
|
||||
await eventBus.send(testMessage);
|
||||
await new Promise((resolve) => {
|
||||
eventBus.logWriter.worker?.on(
|
||||
'message',
|
||||
async function handler003(msg: { command: string; data: any }) {
|
||||
if (msg.command === 'appendMessageToLog') {
|
||||
await confirmIdInAll(testMessage.id);
|
||||
} else if (msg.command === 'confirmMessageSent') {
|
||||
await confirmIdSent(testMessage.id);
|
||||
expect(mockAxiosInstance.request).toHaveBeenCalled();
|
||||
webhookDestination.enabled = false;
|
||||
eventBus.logWriter.worker?.removeListener('message', handler003);
|
||||
resolve(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should send message to sentry ', async () => {
|
||||
const testMessage = new EventMessageGeneric({
|
||||
eventName: 'n8n.test.message' as EventNamesTypes,
|
||||
id: uuid(),
|
||||
});
|
||||
|
||||
const sentryDestination = destinationService['destinations'][
|
||||
testSentryDestination.id!
|
||||
] as MessageEventBusDestinationSentry;
|
||||
|
||||
sentryDestination.enabled = true;
|
||||
|
||||
const mockedSentryCaptureMessage = jest.spyOn(sentryDestination.sentryClient!, 'captureMessage');
|
||||
mockedSentryCaptureMessage.mockImplementation((_m, _level, _hint, _scope) => {
|
||||
eventBus.confirmSent(testMessage, {
|
||||
id: sentryDestination.id,
|
||||
name: sentryDestination.label,
|
||||
});
|
||||
return testMessage.id;
|
||||
});
|
||||
|
||||
await eventBus.send(testMessage);
|
||||
await new Promise((resolve) => {
|
||||
eventBus.logWriter.worker?.on(
|
||||
'message',
|
||||
async function handler004(msg: { command: string; data: any }) {
|
||||
if (msg.command === 'appendMessageToLog') {
|
||||
await confirmIdInAll(testMessage.id);
|
||||
} else if (msg.command === 'confirmMessageSent') {
|
||||
await confirmIdSent(testMessage.id);
|
||||
expect(mockedSentryCaptureMessage).toHaveBeenCalled();
|
||||
sentryDestination.enabled = false;
|
||||
eventBus.logWriter.worker?.removeListener('message', handler004);
|
||||
resolve(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('DELETE /eventbus/destination delete all destinations by id', async () => {
|
||||
const existingDestinations = await destinationService.findDestination();
|
||||
const existingDestinationIds = existingDestinations.reduce<string[]>((acc, d) => {
|
||||
if (d.id) {
|
||||
acc.push(d.id);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// Delete sequentially to avoid race conditions
|
||||
for (const id of existingDestinationIds) {
|
||||
const response = await authOwnerAgent.del('/eventbus/destination').query({ id });
|
||||
expect(response.statusCode).toBe(200);
|
||||
}
|
||||
|
||||
const remainingDestinations = await destinationService.findDestination();
|
||||
expect(remainingDestinations.length).toBe(0);
|
||||
});
|
||||
Reference in New Issue
Block a user