mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
refactor(core): Move integration test utils for insights (#16693)
This commit is contained in:
@@ -22,8 +22,16 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@n8n/backend-common": "workspace:^",
|
||||
"@n8n/config": "workspace:^",
|
||||
"@n8n/constants": "workspace:^",
|
||||
"@n8n/db": "workspace:^",
|
||||
"@n8n/di": "workspace:^",
|
||||
"@n8n/permissions": "workspace:^",
|
||||
"@n8n/typeorm": "catalog:",
|
||||
"jest-mock-extended": "^3.0.4",
|
||||
"reflect-metadata": "catalog:"
|
||||
"n8n-workflow": "workspace:^",
|
||||
"reflect-metadata": "catalog:",
|
||||
"uuid": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*"
|
||||
|
||||
85
packages/@n8n/backend-test-utils/src/db/projects.ts
Normal file
85
packages/@n8n/backend-test-utils/src/db/projects.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { Project, User, ProjectRelation } from '@n8n/db';
|
||||
import { ProjectRelationRepository, ProjectRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { ProjectRole } from '@n8n/permissions';
|
||||
|
||||
import { randomName } from '../random';
|
||||
|
||||
export const linkUserToProject = async (user: User, project: Project, role: ProjectRole) => {
|
||||
const projectRelationRepository = Container.get(ProjectRelationRepository);
|
||||
await projectRelationRepository.save(
|
||||
projectRelationRepository.create({
|
||||
projectId: project.id,
|
||||
userId: user.id,
|
||||
role,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const createTeamProject = async (name?: string, adminUser?: User) => {
|
||||
const projectRepository = Container.get(ProjectRepository);
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: name ?? randomName(),
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
|
||||
if (adminUser) {
|
||||
await linkUserToProject(adminUser, project, 'project:admin');
|
||||
}
|
||||
|
||||
return project;
|
||||
};
|
||||
|
||||
export async function getProjectByNameOrFail(name: string) {
|
||||
return await Container.get(ProjectRepository).findOneOrFail({ where: { name } });
|
||||
}
|
||||
|
||||
export const getPersonalProject = async (user: User): Promise<Project> => {
|
||||
return await Container.get(ProjectRepository).findOneOrFail({
|
||||
where: {
|
||||
projectRelations: {
|
||||
userId: user.id,
|
||||
role: 'project:personalOwner',
|
||||
},
|
||||
type: 'personal',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const findProject = async (id: string): Promise<Project> => {
|
||||
return await Container.get(ProjectRepository).findOneOrFail({
|
||||
where: { id },
|
||||
});
|
||||
};
|
||||
|
||||
export const getProjectRelations = async ({
|
||||
projectId,
|
||||
userId,
|
||||
role,
|
||||
}: Partial<ProjectRelation>): Promise<ProjectRelation[]> => {
|
||||
return await Container.get(ProjectRelationRepository).find({
|
||||
where: { projectId, userId, role },
|
||||
});
|
||||
};
|
||||
|
||||
export const getProjectRoleForUser = async (
|
||||
projectId: string,
|
||||
userId: string,
|
||||
): Promise<ProjectRole | undefined> => {
|
||||
return (
|
||||
await Container.get(ProjectRelationRepository).findOne({
|
||||
select: ['role'],
|
||||
where: { projectId, userId },
|
||||
})
|
||||
)?.role;
|
||||
};
|
||||
|
||||
export const getAllProjectRelations = async ({
|
||||
projectId,
|
||||
}: Partial<ProjectRelation>): Promise<ProjectRelation[]> => {
|
||||
return await Container.get(ProjectRelationRepository).find({
|
||||
where: { projectId },
|
||||
});
|
||||
};
|
||||
187
packages/@n8n/backend-test-utils/src/db/workflows.ts
Normal file
187
packages/@n8n/backend-test-utils/src/db/workflows.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import type { SharedWorkflow, IWorkflowDb } from '@n8n/db';
|
||||
import {
|
||||
Project,
|
||||
User,
|
||||
ProjectRepository,
|
||||
SharedWorkflowRepository,
|
||||
WorkflowRepository,
|
||||
} from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { WorkflowSharingRole } from '@n8n/permissions';
|
||||
import type { DeepPartial } from '@n8n/typeorm';
|
||||
import type { IWorkflowBase } from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export function newWorkflow(attributes: Partial<IWorkflowDb> = {}): IWorkflowDb {
|
||||
const { active, isArchived, name, nodes, connections, versionId, settings } = attributes;
|
||||
|
||||
const workflowEntity = Container.get(WorkflowRepository).create({
|
||||
active: active ?? false,
|
||||
isArchived: isArchived ?? false,
|
||||
name: name ?? 'test workflow',
|
||||
nodes: nodes ?? [
|
||||
{
|
||||
id: 'uuid-1234',
|
||||
name: 'Schedule Trigger',
|
||||
parameters: {},
|
||||
position: [-20, 260],
|
||||
type: 'n8n-nodes-base.scheduleTrigger',
|
||||
typeVersion: 1,
|
||||
},
|
||||
],
|
||||
connections: connections ?? {},
|
||||
versionId: versionId ?? uuid(),
|
||||
settings: settings ?? {},
|
||||
...attributes,
|
||||
});
|
||||
|
||||
return workflowEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a workflow in the DB (without a trigger) and optionally assign it to a user.
|
||||
* @param attributes workflow attributes
|
||||
* @param user user to assign the workflow to
|
||||
*/
|
||||
export async function createWorkflow(
|
||||
attributes: Partial<IWorkflowDb> = {},
|
||||
userOrProject?: User | Project,
|
||||
) {
|
||||
const workflow = await Container.get(WorkflowRepository).save(newWorkflow(attributes));
|
||||
|
||||
if (userOrProject instanceof User) {
|
||||
const user = userOrProject;
|
||||
const project = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(user.id);
|
||||
await Container.get(SharedWorkflowRepository).save(
|
||||
Container.get(SharedWorkflowRepository).create({
|
||||
project,
|
||||
workflow,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (userOrProject instanceof Project) {
|
||||
const project = userOrProject;
|
||||
await Container.get(SharedWorkflowRepository).save(
|
||||
Container.get(SharedWorkflowRepository).create({
|
||||
project,
|
||||
workflow,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
export async function createManyWorkflows(
|
||||
amount: number,
|
||||
attributes: Partial<IWorkflowDb> = {},
|
||||
user?: User,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const workflowRequests = [...Array(amount)].map(
|
||||
async (_) => await createWorkflow(attributes, user),
|
||||
);
|
||||
return await Promise.all(workflowRequests);
|
||||
}
|
||||
|
||||
export async function shareWorkflowWithUsers(workflow: IWorkflowBase, users: User[]) {
|
||||
const sharedWorkflows: Array<DeepPartial<SharedWorkflow>> = await Promise.all(
|
||||
users.map(async (user) => {
|
||||
const project = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
|
||||
user.id,
|
||||
);
|
||||
return {
|
||||
projectId: project.id,
|
||||
workflowId: workflow.id,
|
||||
role: 'workflow:editor',
|
||||
};
|
||||
}),
|
||||
);
|
||||
return await Container.get(SharedWorkflowRepository).save(sharedWorkflows);
|
||||
}
|
||||
|
||||
export async function shareWorkflowWithProjects(
|
||||
workflow: IWorkflowBase,
|
||||
projectsWithRole: Array<{ project: Project; role?: WorkflowSharingRole }>,
|
||||
) {
|
||||
const newSharedWorkflow = await Promise.all(
|
||||
projectsWithRole.map(async ({ project, role }) => {
|
||||
return Container.get(SharedWorkflowRepository).create({
|
||||
workflowId: workflow.id,
|
||||
role: role ?? 'workflow:editor',
|
||||
projectId: project.id,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return await Container.get(SharedWorkflowRepository).save(newSharedWorkflow);
|
||||
}
|
||||
|
||||
export async function getWorkflowSharing(workflow: IWorkflowBase) {
|
||||
return await Container.get(SharedWorkflowRepository).find({
|
||||
where: { workflowId: workflow.id },
|
||||
relations: { project: true },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a workflow in the DB (with a trigger) and optionally assign it to a user.
|
||||
* @param user user to assign the workflow to
|
||||
*/
|
||||
export async function createWorkflowWithTrigger(
|
||||
attributes: Partial<IWorkflowDb> = {},
|
||||
user?: User,
|
||||
) {
|
||||
const workflow = await createWorkflow(
|
||||
{
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1',
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
typeVersion: 1,
|
||||
position: [240, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-2',
|
||||
parameters: { triggerTimes: { item: [{ mode: 'everyMinute' }] } },
|
||||
name: 'Cron',
|
||||
type: 'n8n-nodes-base.cron',
|
||||
typeVersion: 1,
|
||||
position: [500, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-3',
|
||||
parameters: { options: {} },
|
||||
name: 'Set',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 1,
|
||||
position: [780, 300],
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Cron: { main: [[{ node: 'Set', type: NodeConnectionTypes.Main, index: 0 }]] },
|
||||
},
|
||||
...attributes,
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
export async function getAllWorkflows() {
|
||||
return await Container.get(WorkflowRepository).find();
|
||||
}
|
||||
|
||||
export async function getAllSharedWorkflows() {
|
||||
return await Container.get(SharedWorkflowRepository).find();
|
||||
}
|
||||
|
||||
export const getWorkflowById = async (id: string) =>
|
||||
await Container.get(WorkflowRepository).findOneBy({ id });
|
||||
@@ -3,3 +3,10 @@ import { mock } from 'jest-mock-extended';
|
||||
|
||||
export const mockLogger = (): Logger =>
|
||||
mock<Logger>({ scoped: jest.fn().mockReturnValue(mock<Logger>()) });
|
||||
|
||||
export * from './random';
|
||||
export * as testDb from './test-db';
|
||||
export * as testModules from './test-modules';
|
||||
export * from './db/workflows';
|
||||
export * from './db/projects';
|
||||
export * from './mocking';
|
||||
|
||||
12
packages/@n8n/backend-test-utils/src/mocking.ts
Normal file
12
packages/@n8n/backend-test-utils/src/mocking.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Container, type Constructable } from '@n8n/di';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { DeepPartial } from 'ts-essentials';
|
||||
|
||||
export const mockInstance = <T>(
|
||||
serviceClass: Constructable<T>,
|
||||
data: DeepPartial<T> | undefined = undefined,
|
||||
) => {
|
||||
const instance = mock<T>(data);
|
||||
Container.set(serviceClass, instance);
|
||||
return instance;
|
||||
};
|
||||
63
packages/@n8n/backend-test-utils/src/random.ts
Normal file
63
packages/@n8n/backend-test-utils/src/random.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { MIN_PASSWORD_CHAR_LENGTH, MAX_PASSWORD_CHAR_LENGTH } from '@n8n/constants';
|
||||
import { randomInt, randomString, UPPERCASE_LETTERS } from 'n8n-workflow';
|
||||
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export type CredentialPayload = {
|
||||
name: string;
|
||||
type: string;
|
||||
data: ICredentialDataDecryptedObject;
|
||||
isManaged?: boolean;
|
||||
};
|
||||
|
||||
export const randomApiKey = () => `n8n_api_${randomString(40)}`;
|
||||
|
||||
export const chooseRandomly = <T>(array: T[]) => array[randomInt(array.length)];
|
||||
|
||||
const randomUppercaseLetter = () => chooseRandomly(UPPERCASE_LETTERS.split(''));
|
||||
|
||||
export const randomValidPassword = () =>
|
||||
randomString(MIN_PASSWORD_CHAR_LENGTH, MAX_PASSWORD_CHAR_LENGTH - 2) +
|
||||
randomUppercaseLetter() +
|
||||
randomInt(10);
|
||||
|
||||
export const randomInvalidPassword = () =>
|
||||
chooseRandomly([
|
||||
randomString(1, MIN_PASSWORD_CHAR_LENGTH - 1),
|
||||
randomString(MAX_PASSWORD_CHAR_LENGTH + 2, MAX_PASSWORD_CHAR_LENGTH + 100),
|
||||
'abcdefgh', // valid length, no number, no uppercase
|
||||
'abcdefg1', // valid length, has number, no uppercase
|
||||
'abcdefgA', // valid length, no number, has uppercase
|
||||
'abcdefA', // invalid length, no number, has uppercase
|
||||
'abcdef1', // invalid length, has number, no uppercase
|
||||
'abcdeA1', // invalid length, has number, has uppercase
|
||||
'abcdefg', // invalid length, no number, no uppercase
|
||||
]);
|
||||
|
||||
const POPULAR_TOP_LEVEL_DOMAINS = ['com', 'org', 'net', 'io', 'edu'];
|
||||
|
||||
const randomTopLevelDomain = () => chooseRandomly(POPULAR_TOP_LEVEL_DOMAINS);
|
||||
|
||||
export const randomName = () => randomString(4, 8).toLowerCase();
|
||||
|
||||
export const randomEmail = () => `${randomName()}@${randomName()}.${randomTopLevelDomain()}`;
|
||||
|
||||
export const randomCredentialPayload = ({
|
||||
isManaged = false,
|
||||
}: { isManaged?: boolean } = {}): CredentialPayload => ({
|
||||
name: randomName(),
|
||||
type: randomName(),
|
||||
data: { accessToken: randomString(6, 16) },
|
||||
isManaged,
|
||||
});
|
||||
|
||||
export const randomCredentialPayloadWithOauthTokenData = ({
|
||||
isManaged = false,
|
||||
}: { isManaged?: boolean } = {}): CredentialPayload => ({
|
||||
name: randomName(),
|
||||
type: randomName(),
|
||||
data: { accessToken: randomString(6, 16), oauthTokenData: { access_token: randomString(6, 16) } },
|
||||
isManaged,
|
||||
});
|
||||
|
||||
export const uniqueId = () => uuid();
|
||||
80
packages/@n8n/backend-test-utils/src/test-db.ts
Normal file
80
packages/@n8n/backend-test-utils/src/test-db.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import type { entities } from '@n8n/db';
|
||||
import { DbConnection, DbConnectionOptions } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { DataSourceOptions } from '@n8n/typeorm';
|
||||
import { DataSource as Connection } from '@n8n/typeorm';
|
||||
import { randomString } from 'n8n-workflow';
|
||||
|
||||
export const testDbPrefix = 'n8n_test_';
|
||||
|
||||
/**
|
||||
* Generate options for a bootstrap DB connection, to create and drop test databases.
|
||||
*/
|
||||
export const getBootstrapDBOptions = (dbType: 'postgresdb' | 'mysqldb'): DataSourceOptions => {
|
||||
const globalConfig = Container.get(GlobalConfig);
|
||||
const type = dbType === 'postgresdb' ? 'postgres' : 'mysql';
|
||||
return {
|
||||
type,
|
||||
...Container.get(DbConnectionOptions).getOverrides(dbType),
|
||||
database: type,
|
||||
entityPrefix: globalConfig.database.tablePrefix,
|
||||
schema: dbType === 'postgresdb' ? globalConfig.database.postgresdb.schema : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize one test DB per suite run, with bootstrap connection if needed.
|
||||
*/
|
||||
export async function init() {
|
||||
const globalConfig = Container.get(GlobalConfig);
|
||||
const dbType = globalConfig.database.type;
|
||||
const testDbName = `${testDbPrefix}${randomString(6, 10).toLowerCase()}_${Date.now()}`;
|
||||
|
||||
if (dbType === 'postgresdb') {
|
||||
const bootstrapPostgres = await new Connection(
|
||||
getBootstrapDBOptions('postgresdb'),
|
||||
).initialize();
|
||||
await bootstrapPostgres.query(`CREATE DATABASE ${testDbName}`);
|
||||
await bootstrapPostgres.destroy();
|
||||
|
||||
globalConfig.database.postgresdb.database = testDbName;
|
||||
} else if (dbType === 'mysqldb' || dbType === 'mariadb') {
|
||||
const bootstrapMysql = await new Connection(getBootstrapDBOptions('mysqldb')).initialize();
|
||||
await bootstrapMysql.query(`CREATE DATABASE ${testDbName} DEFAULT CHARACTER SET utf8mb4`);
|
||||
await bootstrapMysql.destroy();
|
||||
|
||||
globalConfig.database.mysqldb.database = testDbName;
|
||||
}
|
||||
|
||||
const dbConnection = Container.get(DbConnection);
|
||||
await dbConnection.init();
|
||||
await dbConnection.migrate();
|
||||
}
|
||||
|
||||
export function isReady() {
|
||||
const { connectionState } = Container.get(DbConnection);
|
||||
return connectionState.connected && connectionState.migrated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop test DB, closing bootstrap connection if existing.
|
||||
*/
|
||||
export async function terminate() {
|
||||
const dbConnection = Container.get(DbConnection);
|
||||
await dbConnection.close();
|
||||
dbConnection.connectionState.connected = false;
|
||||
}
|
||||
|
||||
type EntityName = keyof typeof entities | 'InsightsRaw' | 'InsightsByPeriod' | 'InsightsMetadata';
|
||||
|
||||
/**
|
||||
* Truncate specific DB tables in a test DB.
|
||||
*/
|
||||
export async function truncate(entities: EntityName[]) {
|
||||
const connection = Container.get(Connection);
|
||||
|
||||
for (const name of entities) {
|
||||
await connection.getRepository(name).delete({});
|
||||
}
|
||||
}
|
||||
7
packages/@n8n/backend-test-utils/src/test-modules.ts
Normal file
7
packages/@n8n/backend-test-utils/src/test-modules.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { ModuleRegistry } from '@n8n/backend-common';
|
||||
import type { ModuleName } from '@n8n/backend-common';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
export async function loadModules(moduleNames: ModuleName[]) {
|
||||
await Container.get(ModuleRegistry).loadModules(moduleNames);
|
||||
}
|
||||
Reference in New Issue
Block a user