2023-05-02 01:37:19 -07:00
|
|
|
import type { IRun, WorkflowExecuteMode } from 'n8n-workflow';
|
|
|
|
import { LoggerProxy } from 'n8n-workflow';
|
2023-04-12 01:59:14 -07:00
|
|
|
import { QueryFailedError } from 'typeorm';
|
2023-03-17 09:24:05 -07:00
|
|
|
import { mock } from 'jest-mock-extended';
|
|
|
|
|
2022-12-06 06:55:40 -08:00
|
|
|
import config from '@/config';
|
2023-03-17 09:24:05 -07:00
|
|
|
import * as Db from '@/Db';
|
|
|
|
import { User } from '@db/entities/User';
|
2023-05-02 01:37:19 -07:00
|
|
|
import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics';
|
|
|
|
import type { WorkflowStatisticsRepository } from '@db/repositories';
|
2022-12-22 01:14:15 -08:00
|
|
|
import { nodeFetchedData, workflowExecutionCompleted } from '@/events/WorkflowStatistics';
|
2023-02-21 10:21:56 -08:00
|
|
|
import * as UserManagementHelper from '@/UserManagement/UserManagementHelper';
|
2023-03-17 09:24:05 -07:00
|
|
|
import { getLogger } from '@/Logger';
|
2023-02-21 10:21:56 -08:00
|
|
|
import { InternalHooks } from '@/InternalHooks';
|
2022-12-06 06:55:40 -08:00
|
|
|
|
2023-03-17 09:24:05 -07:00
|
|
|
import { mockInstance } from '../integration/shared/utils';
|
2023-04-11 09:43:47 -07:00
|
|
|
import { UserService } from '@/user/user.service';
|
2022-12-06 06:55:40 -08:00
|
|
|
|
2022-12-22 01:14:15 -08:00
|
|
|
jest.mock('@/Db', () => {
|
2022-12-06 06:55:40 -08:00
|
|
|
return {
|
|
|
|
collections: {
|
2023-04-04 09:11:21 -07:00
|
|
|
WorkflowStatistics: mock<WorkflowStatisticsRepository>({
|
2023-04-05 05:51:43 -07:00
|
|
|
metadata: { tableName: 'workflow_statistics' },
|
2023-04-04 09:11:21 -07:00
|
|
|
}),
|
2022-12-06 06:55:40 -08:00
|
|
|
},
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2023-04-11 09:43:47 -07:00
|
|
|
jest.spyOn(UserService, 'updateUserSettings').mockImplementation();
|
|
|
|
|
2022-12-06 06:55:40 -08:00
|
|
|
describe('Events', () => {
|
2023-04-05 05:51:43 -07:00
|
|
|
const dbType = config.getEnv('database.type');
|
2023-03-17 09:24:05 -07:00
|
|
|
const fakeUser = Object.assign(new User(), { id: 'abcde-fghij' });
|
2023-02-21 10:21:56 -08:00
|
|
|
const internalHooks = mockInstance(InternalHooks);
|
|
|
|
|
2023-03-17 09:24:05 -07:00
|
|
|
jest.spyOn(UserManagementHelper, 'getWorkflowOwner').mockResolvedValue(fakeUser);
|
|
|
|
|
|
|
|
const workflowStatisticsRepository = Db.collections.WorkflowStatistics as ReturnType<
|
|
|
|
typeof mock<WorkflowStatisticsRepository>
|
|
|
|
>;
|
|
|
|
|
2022-12-06 06:55:40 -08:00
|
|
|
beforeAll(() => {
|
|
|
|
config.set('diagnostics.enabled', true);
|
|
|
|
config.set('deployment.type', 'n8n-testing');
|
2022-12-20 01:52:01 -08:00
|
|
|
LoggerProxy.init(getLogger());
|
2022-12-06 06:55:40 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
afterAll(() => {
|
|
|
|
jest.clearAllTimers();
|
|
|
|
jest.useRealTimers();
|
|
|
|
});
|
|
|
|
|
|
|
|
beforeEach(() => {
|
2023-04-05 05:51:43 -07:00
|
|
|
if (dbType === 'sqlite') {
|
|
|
|
workflowStatisticsRepository.findOne.mockClear();
|
|
|
|
} else {
|
|
|
|
workflowStatisticsRepository.query.mockClear();
|
|
|
|
}
|
|
|
|
|
2023-02-21 10:21:56 -08:00
|
|
|
internalHooks.onFirstProductionWorkflowSuccess.mockClear();
|
|
|
|
internalHooks.onFirstWorkflowDataLoad.mockClear();
|
2022-12-06 06:55:40 -08:00
|
|
|
});
|
|
|
|
|
2023-04-05 05:51:43 -07:00
|
|
|
const mockDBCall = (count = 1) => {
|
|
|
|
if (dbType === 'sqlite') {
|
|
|
|
workflowStatisticsRepository.findOne.mockResolvedValueOnce(
|
|
|
|
mock<WorkflowStatistics>({ count }),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
const result = dbType === 'postgresdb' ? [{ count }] : { affectedRows: count };
|
|
|
|
workflowStatisticsRepository.query.mockImplementationOnce(async (query) =>
|
|
|
|
query.startsWith('INSERT INTO') ? result : null,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
2022-12-06 06:55:40 -08:00
|
|
|
|
|
|
|
describe('workflowExecutionCompleted', () => {
|
|
|
|
test('should create metrics for production successes', async () => {
|
|
|
|
// Call the function with a production success result, ensure metrics hook gets called
|
|
|
|
const workflow = {
|
|
|
|
id: '1',
|
|
|
|
name: '',
|
|
|
|
active: false,
|
|
|
|
createdAt: new Date(),
|
|
|
|
updatedAt: new Date(),
|
|
|
|
nodes: [],
|
|
|
|
connections: {},
|
|
|
|
};
|
2023-03-17 09:24:05 -07:00
|
|
|
const runData: IRun = {
|
2022-12-06 06:55:40 -08:00
|
|
|
finished: true,
|
2023-03-17 09:24:05 -07:00
|
|
|
status: 'success',
|
2022-12-06 06:55:40 -08:00
|
|
|
data: { resultData: { runData: {} } },
|
|
|
|
mode: 'internal' as WorkflowExecuteMode,
|
|
|
|
startedAt: new Date(),
|
|
|
|
};
|
2023-04-05 05:51:43 -07:00
|
|
|
mockDBCall();
|
|
|
|
|
2022-12-06 06:55:40 -08:00
|
|
|
await workflowExecutionCompleted(workflow, runData);
|
2023-02-21 10:21:56 -08:00
|
|
|
expect(internalHooks.onFirstProductionWorkflowSuccess).toBeCalledTimes(1);
|
|
|
|
expect(internalHooks.onFirstProductionWorkflowSuccess).toHaveBeenNthCalledWith(1, {
|
2023-03-17 09:24:05 -07:00
|
|
|
user_id: fakeUser.id,
|
2023-01-02 08:42:32 -08:00
|
|
|
workflow_id: workflow.id,
|
2022-12-06 06:55:40 -08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('should only create metrics for production successes', async () => {
|
|
|
|
// Call the function with a non production success result, ensure metrics hook is never called
|
|
|
|
const workflow = {
|
|
|
|
id: '1',
|
|
|
|
name: '',
|
|
|
|
active: false,
|
|
|
|
createdAt: new Date(),
|
|
|
|
updatedAt: new Date(),
|
|
|
|
nodes: [],
|
|
|
|
connections: {},
|
|
|
|
};
|
2023-03-17 09:24:05 -07:00
|
|
|
const runData: IRun = {
|
2022-12-06 06:55:40 -08:00
|
|
|
finished: false,
|
2023-03-17 09:24:05 -07:00
|
|
|
status: 'failed',
|
2022-12-06 06:55:40 -08:00
|
|
|
data: { resultData: { runData: {} } },
|
|
|
|
mode: 'internal' as WorkflowExecuteMode,
|
|
|
|
startedAt: new Date(),
|
|
|
|
};
|
|
|
|
await workflowExecutionCompleted(workflow, runData);
|
2023-02-21 10:21:56 -08:00
|
|
|
expect(internalHooks.onFirstProductionWorkflowSuccess).toBeCalledTimes(0);
|
2022-12-06 06:55:40 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
test('should not send metrics for updated entries', async () => {
|
2023-01-30 08:34:26 -08:00
|
|
|
// Call the function with a fail insert, ensure update is called *and* metrics aren't sent
|
2022-12-06 06:55:40 -08:00
|
|
|
const workflow = {
|
2023-01-30 08:34:26 -08:00
|
|
|
id: '1',
|
2022-12-06 06:55:40 -08:00
|
|
|
name: '',
|
|
|
|
active: false,
|
|
|
|
createdAt: new Date(),
|
|
|
|
updatedAt: new Date(),
|
|
|
|
nodes: [],
|
|
|
|
connections: {},
|
|
|
|
};
|
2023-03-17 09:24:05 -07:00
|
|
|
const runData: IRun = {
|
2022-12-06 06:55:40 -08:00
|
|
|
finished: true,
|
2023-03-17 09:24:05 -07:00
|
|
|
status: 'success',
|
2022-12-06 06:55:40 -08:00
|
|
|
data: { resultData: { runData: {} } },
|
|
|
|
mode: 'internal' as WorkflowExecuteMode,
|
|
|
|
startedAt: new Date(),
|
|
|
|
};
|
2023-04-05 05:51:43 -07:00
|
|
|
mockDBCall(2);
|
2022-12-06 06:55:40 -08:00
|
|
|
await workflowExecutionCompleted(workflow, runData);
|
2023-02-21 10:21:56 -08:00
|
|
|
expect(internalHooks.onFirstProductionWorkflowSuccess).toBeCalledTimes(0);
|
2022-12-06 06:55:40 -08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('nodeFetchedData', () => {
|
|
|
|
test('should create metrics when the db is updated', async () => {
|
|
|
|
// Call the function with a production success result, ensure metrics hook gets called
|
|
|
|
const workflowId = '1';
|
|
|
|
const node = {
|
|
|
|
id: 'abcde',
|
|
|
|
name: 'test node',
|
|
|
|
typeVersion: 1,
|
|
|
|
type: '',
|
|
|
|
position: [0, 0] as [number, number],
|
|
|
|
parameters: {},
|
|
|
|
};
|
|
|
|
await nodeFetchedData(workflowId, node);
|
2023-02-21 10:21:56 -08:00
|
|
|
expect(internalHooks.onFirstWorkflowDataLoad).toBeCalledTimes(1);
|
|
|
|
expect(internalHooks.onFirstWorkflowDataLoad).toHaveBeenNthCalledWith(1, {
|
2023-03-17 09:24:05 -07:00
|
|
|
user_id: fakeUser.id,
|
2023-01-02 08:42:32 -08:00
|
|
|
workflow_id: workflowId,
|
2022-12-06 06:55:40 -08:00
|
|
|
node_type: node.type,
|
|
|
|
node_id: node.id,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('should create metrics with credentials when the db is updated', async () => {
|
|
|
|
// Call the function with a production success result, ensure metrics hook gets called
|
|
|
|
const workflowId = '1';
|
|
|
|
const node = {
|
|
|
|
id: 'abcde',
|
|
|
|
name: 'test node',
|
|
|
|
typeVersion: 1,
|
|
|
|
type: '',
|
|
|
|
position: [0, 0] as [number, number],
|
|
|
|
parameters: {},
|
|
|
|
credentials: {
|
|
|
|
testCredentials: {
|
|
|
|
id: '1',
|
|
|
|
name: 'Test Credentials',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
await nodeFetchedData(workflowId, node);
|
2023-02-21 10:21:56 -08:00
|
|
|
expect(internalHooks.onFirstWorkflowDataLoad).toBeCalledTimes(1);
|
|
|
|
expect(internalHooks.onFirstWorkflowDataLoad).toHaveBeenNthCalledWith(1, {
|
2023-03-17 09:24:05 -07:00
|
|
|
user_id: fakeUser.id,
|
2023-01-02 08:42:32 -08:00
|
|
|
workflow_id: workflowId,
|
2022-12-06 06:55:40 -08:00
|
|
|
node_type: node.type,
|
|
|
|
node_id: node.id,
|
|
|
|
credential_type: 'testCredentials',
|
|
|
|
credential_id: node.credentials.testCredentials.id,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('should not send metrics for entries that already have the flag set', async () => {
|
|
|
|
// Fetch data for workflow 2 which is set up to not be altered in the mocks
|
2023-03-17 09:24:05 -07:00
|
|
|
workflowStatisticsRepository.insert.mockImplementationOnce(() => {
|
2023-01-30 08:34:26 -08:00
|
|
|
throw new QueryFailedError('invalid insert', [], '');
|
|
|
|
});
|
|
|
|
const workflowId = '1';
|
2022-12-06 06:55:40 -08:00
|
|
|
const node = {
|
|
|
|
id: 'abcde',
|
|
|
|
name: 'test node',
|
|
|
|
typeVersion: 1,
|
|
|
|
type: '',
|
|
|
|
position: [0, 0] as [number, number],
|
|
|
|
parameters: {},
|
|
|
|
};
|
|
|
|
await nodeFetchedData(workflowId, node);
|
2023-02-21 10:21:56 -08:00
|
|
|
expect(internalHooks.onFirstWorkflowDataLoad).toBeCalledTimes(0);
|
2022-12-06 06:55:40 -08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|