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,173 @@
|
||||
import type { CredentialPayload } from '@n8n/backend-test-utils';
|
||||
import type { Project, User, ICredentialsDb } from '@n8n/db';
|
||||
import {
|
||||
CredentialsEntity,
|
||||
CredentialsRepository,
|
||||
ProjectRepository,
|
||||
SharedCredentialsRepository,
|
||||
} from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { CredentialSharingRole } from '@n8n/permissions';
|
||||
|
||||
export async function encryptCredentialData(
|
||||
credential: CredentialsEntity,
|
||||
): Promise<ICredentialsDb> {
|
||||
const { createCredentialsFromCredentialsEntity } = await import('@/credentials-helper');
|
||||
const coreCredential = createCredentialsFromCredentialsEntity(credential, true);
|
||||
|
||||
// @ts-ignore
|
||||
coreCredential.setData(credential.data);
|
||||
|
||||
return Object.assign(credential, coreCredential.getDataToSave());
|
||||
}
|
||||
|
||||
export async function decryptCredentialData(credential: ICredentialsDb): Promise<unknown> {
|
||||
const { createCredentialsFromCredentialsEntity } = await import('@/credentials-helper');
|
||||
const coreCredential = createCredentialsFromCredentialsEntity(credential);
|
||||
|
||||
return coreCredential.getData();
|
||||
}
|
||||
|
||||
const emptyAttributes = {
|
||||
name: 'test',
|
||||
type: 'test',
|
||||
data: '',
|
||||
};
|
||||
|
||||
export async function createManyCredentials(
|
||||
amount: number,
|
||||
attributes: Partial<CredentialsEntity> = emptyAttributes,
|
||||
) {
|
||||
return await Promise.all(
|
||||
Array(amount)
|
||||
.fill(0)
|
||||
.map(async () => await createCredentials(attributes)),
|
||||
);
|
||||
}
|
||||
|
||||
export async function createCredentials(
|
||||
attributes: Partial<CredentialsEntity> = emptyAttributes,
|
||||
project?: Project,
|
||||
) {
|
||||
const credentialsRepository = Container.get(CredentialsRepository);
|
||||
const credentials = await credentialsRepository.save(credentialsRepository.create(attributes));
|
||||
|
||||
if (project) {
|
||||
await Container.get(SharedCredentialsRepository).save(
|
||||
Container.get(SharedCredentialsRepository).create({
|
||||
project,
|
||||
credentials,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a credential to the test DB, sharing it with a user.
|
||||
*/
|
||||
export async function saveCredential(
|
||||
credentialPayload: CredentialPayload,
|
||||
options:
|
||||
| { user: User; role: CredentialSharingRole }
|
||||
| {
|
||||
project: Project;
|
||||
role: CredentialSharingRole;
|
||||
},
|
||||
) {
|
||||
const role = options.role;
|
||||
const newCredential = new CredentialsEntity();
|
||||
|
||||
Object.assign(newCredential, credentialPayload);
|
||||
|
||||
await encryptCredentialData(newCredential);
|
||||
|
||||
const savedCredential = await Container.get(CredentialsRepository).save(newCredential);
|
||||
|
||||
savedCredential.data = newCredential.data;
|
||||
|
||||
if ('user' in options) {
|
||||
const user = options.user;
|
||||
const personalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
|
||||
user.id,
|
||||
);
|
||||
|
||||
await Container.get(SharedCredentialsRepository).save({
|
||||
user,
|
||||
credentials: savedCredential,
|
||||
role,
|
||||
project: personalProject,
|
||||
});
|
||||
} else {
|
||||
const project = options.project;
|
||||
|
||||
await Container.get(SharedCredentialsRepository).save({
|
||||
credentials: savedCredential,
|
||||
role,
|
||||
project,
|
||||
});
|
||||
}
|
||||
|
||||
return savedCredential;
|
||||
}
|
||||
|
||||
export async function shareCredentialWithUsers(credential: CredentialsEntity, users: User[]) {
|
||||
const newSharedCredentials = await Promise.all(
|
||||
users.map(async (user) => {
|
||||
const personalProject = await Container.get(
|
||||
ProjectRepository,
|
||||
).getPersonalProjectForUserOrFail(user.id);
|
||||
|
||||
return Container.get(SharedCredentialsRepository).create({
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
projectId: personalProject.id,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return await Container.get(SharedCredentialsRepository).save(newSharedCredentials);
|
||||
}
|
||||
|
||||
export async function shareCredentialWithProjects(
|
||||
credential: CredentialsEntity,
|
||||
projects: Project[],
|
||||
) {
|
||||
const newSharedCredentials = await Promise.all(
|
||||
projects.map(async (project) => {
|
||||
return Container.get(SharedCredentialsRepository).create({
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
projectId: project.id,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return await Container.get(SharedCredentialsRepository).save(newSharedCredentials);
|
||||
}
|
||||
|
||||
export function affixRoleToSaveCredential(role: CredentialSharingRole) {
|
||||
return async (
|
||||
credentialPayload: CredentialPayload,
|
||||
options: { user: User } | { project: Project },
|
||||
) => await saveCredential(credentialPayload, { ...options, role });
|
||||
}
|
||||
|
||||
export async function getAllCredentials() {
|
||||
return await Container.get(CredentialsRepository).find();
|
||||
}
|
||||
|
||||
export const getCredentialById = async (id: string) =>
|
||||
await Container.get(CredentialsRepository).findOneBy({ id });
|
||||
|
||||
export async function getAllSharedCredentials() {
|
||||
return await Container.get(SharedCredentialsRepository).find();
|
||||
}
|
||||
|
||||
export async function getCredentialSharings(credential: CredentialsEntity) {
|
||||
return await Container.get(SharedCredentialsRepository).findBy({
|
||||
credentialsId: credential.id,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { CreateDataTableColumnDto } from '@n8n/api-types';
|
||||
import { randomName } from '@n8n/backend-test-utils';
|
||||
import type { Project } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { DataTableRows } from 'n8n-workflow';
|
||||
|
||||
import { DataTableColumnRepository } from '@/modules/data-table/data-table-column.repository';
|
||||
import { DataTableRowsRepository } from '@/modules/data-table/data-table-rows.repository';
|
||||
import { DataTableRepository } from '@/modules/data-table/data-table.repository';
|
||||
|
||||
export const createDataTable = async (
|
||||
project: Project,
|
||||
options: {
|
||||
name?: string;
|
||||
columns?: CreateDataTableColumnDto[];
|
||||
data?: DataTableRows;
|
||||
updatedAt?: Date;
|
||||
} = {},
|
||||
) => {
|
||||
const dataTableRepository = Container.get(DataTableRepository);
|
||||
const dataTable = await dataTableRepository.createDataTable(
|
||||
project.id,
|
||||
options.name ?? randomName(),
|
||||
options.columns ?? [],
|
||||
);
|
||||
|
||||
if (options.updatedAt) {
|
||||
await dataTableRepository.update(dataTable.id, {
|
||||
updatedAt: options.updatedAt,
|
||||
});
|
||||
dataTable.updatedAt = options.updatedAt;
|
||||
}
|
||||
|
||||
if (options.data) {
|
||||
const dataTableColumnRepository = Container.get(DataTableColumnRepository);
|
||||
const columns = await dataTableColumnRepository.getColumns(dataTable.id);
|
||||
|
||||
const dataTableRowsRepository = Container.get(DataTableRowsRepository);
|
||||
await dataTableRowsRepository.insertRows(dataTable.id, options.data, columns, 'count');
|
||||
}
|
||||
|
||||
return dataTable;
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { TestRunRepository, TestCaseExecutionRepository } from '@n8n/db';
|
||||
import type {
|
||||
TestRun,
|
||||
TestCaseExecution,
|
||||
AggregatedTestRunMetrics,
|
||||
TestCaseExecutionErrorCode,
|
||||
TestRunErrorCode,
|
||||
} from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
* Creates a test run for a workflow
|
||||
*/
|
||||
export const createTestRun = async (
|
||||
workflowId: string,
|
||||
options: {
|
||||
status?: TestRun['status'];
|
||||
runAt?: Date | null;
|
||||
completedAt?: Date | null;
|
||||
metrics?: AggregatedTestRunMetrics;
|
||||
errorCode?: TestRunErrorCode;
|
||||
errorDetails?: IDataObject;
|
||||
} = {},
|
||||
) => {
|
||||
const testRunRepository = Container.get(TestRunRepository);
|
||||
|
||||
const testRun = testRunRepository.create({
|
||||
workflow: { id: workflowId },
|
||||
status: options.status ?? 'new',
|
||||
runAt: options.runAt ?? null,
|
||||
completedAt: options.completedAt ?? null,
|
||||
metrics: options.metrics ?? {},
|
||||
errorCode: options.errorCode,
|
||||
errorDetails: options.errorDetails,
|
||||
});
|
||||
|
||||
return await testRunRepository.save(testRun);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a test case execution for a test run
|
||||
*/
|
||||
export const createTestCaseExecution = async (
|
||||
testRunId: string,
|
||||
options: {
|
||||
status?: TestCaseExecution['status'];
|
||||
runAt?: Date | null;
|
||||
completedAt?: Date | null;
|
||||
metrics?: Record<string, number>;
|
||||
errorCode?: TestCaseExecutionErrorCode;
|
||||
errorDetails?: IDataObject;
|
||||
executionId?: string;
|
||||
pastExecutionId?: string;
|
||||
} = {},
|
||||
) => {
|
||||
const testCaseExecutionRepository = Container.get(TestCaseExecutionRepository);
|
||||
|
||||
const testCaseExecution = testCaseExecutionRepository.create({
|
||||
testRun: { id: testRunId },
|
||||
status: options.status ?? 'success',
|
||||
runAt: options.runAt ?? null,
|
||||
completedAt: options.completedAt ?? null,
|
||||
metrics: options.metrics ?? {},
|
||||
errorCode: options.errorCode,
|
||||
errorDetails: options.errorDetails,
|
||||
executionId: options.executionId,
|
||||
});
|
||||
|
||||
return await testCaseExecutionRepository.save(testCaseExecution);
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import type { ExecutionEntity, ExecutionData } from '@n8n/db';
|
||||
import {
|
||||
ExecutionDataRepository,
|
||||
ExecutionMetadataRepository,
|
||||
ExecutionRepository,
|
||||
AnnotationTagRepository,
|
||||
} from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { AnnotationVote, ExecutionStatus, IWorkflowBase } from 'n8n-workflow';
|
||||
|
||||
import { ExecutionService } from '@/executions/execution.service';
|
||||
import { Telemetry } from '@/telemetry';
|
||||
|
||||
mockInstance(Telemetry);
|
||||
|
||||
export async function createManyExecutions(
|
||||
amount: number,
|
||||
workflow: IWorkflowBase,
|
||||
callback: (workflow: IWorkflowBase) => Promise<ExecutionEntity>,
|
||||
) {
|
||||
const executionsRequests = [...Array(amount)].map(async (_) => await callback(workflow));
|
||||
return await Promise.all(executionsRequests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a execution in the DB and assign it to a workflow.
|
||||
*/
|
||||
export async function createExecution(
|
||||
attributes: Partial<
|
||||
Omit<ExecutionEntity, 'metadata'> &
|
||||
ExecutionData & { metadata: Array<{ key: string; value: string }> }
|
||||
>,
|
||||
workflow: IWorkflowBase,
|
||||
) {
|
||||
const {
|
||||
data,
|
||||
finished,
|
||||
mode,
|
||||
startedAt,
|
||||
stoppedAt,
|
||||
waitTill,
|
||||
status,
|
||||
deletedAt,
|
||||
metadata,
|
||||
createdAt,
|
||||
} = attributes;
|
||||
|
||||
const execution = await Container.get(ExecutionRepository).save({
|
||||
finished: finished ?? true,
|
||||
mode: mode ?? 'manual',
|
||||
createdAt: createdAt ?? new Date(),
|
||||
startedAt: startedAt === undefined ? new Date() : startedAt,
|
||||
...(workflow !== undefined && { workflowId: workflow.id }),
|
||||
stoppedAt: stoppedAt ?? new Date(),
|
||||
waitTill: waitTill ?? null,
|
||||
status: status ?? 'success',
|
||||
deletedAt,
|
||||
});
|
||||
|
||||
if (metadata?.length) {
|
||||
const metadataToSave = metadata.map(({ key, value }) => ({
|
||||
key,
|
||||
value,
|
||||
execution: { id: execution.id },
|
||||
}));
|
||||
|
||||
await Container.get(ExecutionMetadataRepository).save(metadataToSave);
|
||||
}
|
||||
|
||||
await Container.get(ExecutionDataRepository).save({
|
||||
data: data ?? '[]',
|
||||
workflowData: workflow ?? {},
|
||||
executionId: execution.id,
|
||||
});
|
||||
|
||||
return execution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a successful execution in the DB and assign it to a workflow.
|
||||
*/
|
||||
export async function createSuccessfulExecution(workflow: IWorkflowBase) {
|
||||
return await createExecution({ finished: true, status: 'success' }, workflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an error execution in the DB and assign it to a workflow.
|
||||
*/
|
||||
export async function createErrorExecution(workflow: IWorkflowBase) {
|
||||
return await createExecution(
|
||||
{ finished: false, stoppedAt: new Date(), status: 'error' },
|
||||
workflow,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a waiting execution in the DB and assign it to a workflow.
|
||||
*/
|
||||
export async function createWaitingExecution(workflow: IWorkflowBase) {
|
||||
return await createExecution(
|
||||
{ finished: false, waitTill: new Date(), status: 'waiting' },
|
||||
workflow,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an execution with a given status in the DB and assign it to a workflow.
|
||||
*/
|
||||
export async function createdExecutionWithStatus(workflow: IWorkflowBase, status: ExecutionStatus) {
|
||||
const execution: Partial<ExecutionEntity> = {
|
||||
status,
|
||||
finished: status === 'success' ? true : false,
|
||||
stoppedAt: ['crashed', 'error'].includes(status) ? new Date() : undefined,
|
||||
waitTill: status === 'waiting' ? new Date() : undefined,
|
||||
};
|
||||
|
||||
return await createExecution(execution, workflow);
|
||||
}
|
||||
|
||||
export async function annotateExecution(
|
||||
executionId: string,
|
||||
annotation: { vote?: AnnotationVote | null; tags?: string[] },
|
||||
sharedWorkflowIds: string[],
|
||||
) {
|
||||
await Container.get(ExecutionService).annotate(executionId, annotation, sharedWorkflowIds);
|
||||
}
|
||||
|
||||
export async function getAllExecutions() {
|
||||
return await Container.get(ExecutionRepository).find();
|
||||
}
|
||||
|
||||
export async function createAnnotationTags(annotationTags: string[]) {
|
||||
const tagRepository = Container.get(AnnotationTagRepository);
|
||||
return await tagRepository.save(annotationTags.map((name) => tagRepository.create({ name })));
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { randomName } from '@n8n/backend-test-utils';
|
||||
import type { Folder, Project, TagEntity } from '@n8n/db';
|
||||
import { FolderRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
export const createFolder = async (
|
||||
project: Project,
|
||||
options: {
|
||||
name?: string;
|
||||
parentFolder?: Folder;
|
||||
tags?: TagEntity[];
|
||||
updatedAt?: Date;
|
||||
createdAt?: Date;
|
||||
} = {},
|
||||
) => {
|
||||
const folderRepository = Container.get(FolderRepository);
|
||||
const folder = await folderRepository.save(
|
||||
folderRepository.create({
|
||||
name: options.name ?? randomName(),
|
||||
homeProject: project,
|
||||
parentFolder: options.parentFolder ?? null,
|
||||
tags: options.tags ?? [],
|
||||
updatedAt: options.updatedAt ?? new Date(),
|
||||
createdAt: options.updatedAt ?? new Date(),
|
||||
}),
|
||||
);
|
||||
|
||||
return folder;
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
import { RoleCacheService } from '@/services/role-cache.service';
|
||||
import { Role, RoleRepository, Scope, ScopeRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { Scope as ScopeType } from '@n8n/permissions';
|
||||
|
||||
/**
|
||||
* Creates a test role with given parameters
|
||||
*/
|
||||
export async function createRole(overrides: Partial<Role> = {}): Promise<Role> {
|
||||
const roleRepository = Container.get(RoleRepository);
|
||||
const roleCacheService = Container.get(RoleCacheService);
|
||||
|
||||
const defaultRole: Partial<Role> = {
|
||||
slug: `test-role-${Math.random().toString(36).substring(7)}`,
|
||||
displayName: `Test Role ${Math.random().toString(36).substring(7)}`,
|
||||
description: 'A test role for integration testing',
|
||||
systemRole: false,
|
||||
roleType: 'project',
|
||||
scopes: [],
|
||||
};
|
||||
|
||||
const roleData = { ...defaultRole, ...overrides };
|
||||
const role = Object.assign(new Role(), roleData);
|
||||
|
||||
const createdRole = await roleRepository.save(role);
|
||||
|
||||
// Force refresh the role cache to include the newly created role (important when running concurrent tests)
|
||||
await roleCacheService.refreshCache();
|
||||
return createdRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a system role (cannot be deleted/modified)
|
||||
*/
|
||||
export async function createSystemRole(overrides: Partial<Role> = {}): Promise<Role> {
|
||||
return await createRole({
|
||||
systemRole: true,
|
||||
slug: `system-role-${Math.random().toString(36).substring(7)}`,
|
||||
displayName: 'System Test Role',
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a custom role with specific scopes
|
||||
*/
|
||||
export async function createCustomRoleWithScopes(
|
||||
scopes: Scope[],
|
||||
overrides: Partial<Role> = {},
|
||||
): Promise<Role> {
|
||||
return await createRole({
|
||||
scopes,
|
||||
systemRole: false,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a custom role with specific scope slugs (using existing permission system scopes)
|
||||
*/
|
||||
export async function createCustomRoleWithScopeSlugs(
|
||||
scopeSlugs: string[],
|
||||
overrides: Partial<Role> = {},
|
||||
): Promise<Role> {
|
||||
const scopeRepository = Container.get(ScopeRepository);
|
||||
|
||||
// Find existing scopes by their slugs
|
||||
const scopes = await scopeRepository.findByList(scopeSlugs);
|
||||
|
||||
if (scopes.length !== scopeSlugs.length) {
|
||||
const missingScopes = scopeSlugs.filter((slug) => !scopes.some((scope) => scope.slug === slug));
|
||||
throw new Error(
|
||||
`Could not find all scopes. Expected ${scopeSlugs.length}, found ${scopes.length}, missing: ${missingScopes.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
return await createRole({
|
||||
scopes,
|
||||
systemRole: false,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a test scope with given parameters
|
||||
*/
|
||||
export async function createScope(overrides: Partial<Scope> = {}): Promise<Scope> {
|
||||
const scopeRepository = Container.get(ScopeRepository);
|
||||
|
||||
const defaultScope: Partial<Scope> = {
|
||||
slug: `test:scope:${Math.random().toString(36).substring(7)}` as ScopeType,
|
||||
displayName: 'Test Scope',
|
||||
description: 'A test scope for integration testing',
|
||||
};
|
||||
|
||||
const scopeData = { ...defaultScope, ...overrides };
|
||||
const scope = Object.assign(new Scope(), scopeData);
|
||||
|
||||
return await scopeRepository.save(scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates multiple test scopes
|
||||
*/
|
||||
export async function createScopes(
|
||||
count: number,
|
||||
baseOverrides: Partial<Scope> = {},
|
||||
): Promise<Scope[]> {
|
||||
const scopes: Scope[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const scope = await createScope({
|
||||
slug: `test:scope:${i}:${Math.random().toString(36).substring(7)}` as ScopeType,
|
||||
displayName: `Test Scope ${i}`,
|
||||
...baseOverrides,
|
||||
});
|
||||
scopes.push(scope);
|
||||
}
|
||||
|
||||
return scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates predefined test scopes for common test scenarios
|
||||
*/
|
||||
export async function createTestScopes(): Promise<{
|
||||
readScope: Scope;
|
||||
writeScope: Scope;
|
||||
deleteScope: Scope;
|
||||
adminScope: Scope;
|
||||
}> {
|
||||
const [readScope, writeScope, deleteScope, adminScope] = await Promise.all([
|
||||
createScope({
|
||||
slug: 'test:read' as ScopeType,
|
||||
displayName: 'Test Read',
|
||||
description: 'Test read access',
|
||||
}),
|
||||
createScope({
|
||||
slug: 'test:write' as ScopeType,
|
||||
displayName: 'Test Write',
|
||||
description: 'Test write access',
|
||||
}),
|
||||
createScope({
|
||||
slug: 'test:delete' as ScopeType,
|
||||
displayName: 'Test Delete',
|
||||
description: 'Test delete access',
|
||||
}),
|
||||
createScope({
|
||||
slug: 'test:admin' as ScopeType,
|
||||
displayName: 'Test Admin',
|
||||
description: 'Test admin access',
|
||||
}),
|
||||
]);
|
||||
|
||||
return { readScope, writeScope, deleteScope, adminScope };
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up test roles and scopes
|
||||
*/
|
||||
export async function cleanupRolesAndScopes(): Promise<void> {
|
||||
const roleRepository = Container.get(RoleRepository);
|
||||
const scopeRepository = Container.get(ScopeRepository);
|
||||
|
||||
// Delete test roles (excluding system roles for safety)
|
||||
const testRoles = await roleRepository
|
||||
.createQueryBuilder('role')
|
||||
.where('role.slug LIKE :testPattern', { testPattern: 'test-role-%' })
|
||||
.orWhere('role.slug LIKE :systemPattern', { systemPattern: 'system-role-%' })
|
||||
.getMany();
|
||||
|
||||
for (const role of testRoles) {
|
||||
await roleRepository.delete({ slug: role.slug });
|
||||
}
|
||||
|
||||
// Delete test scopes
|
||||
const testScopes = await scopeRepository
|
||||
.createQueryBuilder('scope')
|
||||
.where('scope.slug LIKE :pattern', { pattern: 'test:%' })
|
||||
.getMany();
|
||||
|
||||
for (const scope of testScopes) {
|
||||
await scopeRepository.delete({ slug: scope.slug });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { randomName } from '@n8n/backend-test-utils';
|
||||
import type { TagEntity, WorkflowEntity } from '@n8n/db';
|
||||
import { generateNanoId, TagRepository, WorkflowTagMappingRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { IWorkflowBase } from 'n8n-workflow';
|
||||
|
||||
export async function createTag(attributes: Partial<TagEntity> = {}, workflow?: IWorkflowBase) {
|
||||
const { name } = attributes;
|
||||
|
||||
const tag = await Container.get(TagRepository).save({
|
||||
id: generateNanoId(),
|
||||
name: name ?? randomName(),
|
||||
...attributes,
|
||||
});
|
||||
|
||||
if (workflow) {
|
||||
const mappingRepository = Container.get(WorkflowTagMappingRepository);
|
||||
const mapping = mappingRepository.create({ tagId: tag.id, workflowId: workflow.id });
|
||||
await mappingRepository.save(mapping);
|
||||
}
|
||||
|
||||
return tag;
|
||||
}
|
||||
|
||||
export async function updateTag(tag: TagEntity, attributes: Partial<TagEntity>) {
|
||||
const tagRepository = Container.get(TagRepository);
|
||||
const updatedTag = tagRepository.merge(tag, attributes);
|
||||
return await tagRepository.save(updatedTag);
|
||||
}
|
||||
|
||||
export async function assignTagToWorkflow(tag: TagEntity, workflow: WorkflowEntity) {
|
||||
const mappingRepository = Container.get(WorkflowTagMappingRepository);
|
||||
|
||||
// Check if mapping already exists
|
||||
const existingMapping = await mappingRepository.findOne({
|
||||
where: {
|
||||
tagId: tag.id,
|
||||
workflowId: workflow.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingMapping) {
|
||||
return existingMapping;
|
||||
}
|
||||
|
||||
// Create new mapping
|
||||
const mapping = mappingRepository.create({
|
||||
tagId: tag.id,
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
|
||||
return await mappingRepository.save(mapping);
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import { randomEmail, randomName, randomValidPassword } from '@n8n/backend-test-utils';
|
||||
import {
|
||||
AuthIdentity,
|
||||
AuthIdentityRepository,
|
||||
GLOBAL_ADMIN_ROLE,
|
||||
GLOBAL_CHAT_USER_ROLE,
|
||||
GLOBAL_MEMBER_ROLE,
|
||||
GLOBAL_OWNER_ROLE,
|
||||
type Role,
|
||||
UserRepository,
|
||||
} from '@n8n/db';
|
||||
import { type User } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { ApiKeyScope } from '@n8n/permissions';
|
||||
import { getApiKeyScopesForRole } from '@n8n/permissions';
|
||||
import { hash } from 'bcryptjs';
|
||||
|
||||
import { MfaService } from '@/mfa/mfa.service';
|
||||
import { TOTPService } from '@/mfa/totp.service';
|
||||
import { PublicApiKeyService } from '@/services/public-api-key.service';
|
||||
import type { DeepPartial } from '@n8n/typeorm';
|
||||
|
||||
type ApiKeyOptions = {
|
||||
expiresAt?: number | null;
|
||||
scopes?: ApiKeyScope[];
|
||||
};
|
||||
|
||||
// pre-computed bcrypt hash for the string 'password', using `await hash('password', 10)`
|
||||
const passwordHash = '$2a$10$njedH7S6V5898mj6p0Jr..IGY9Ms.qNwR7RbSzzX9yubJocKfvGGK';
|
||||
|
||||
// A null password value means that no password will be set in the database
|
||||
// rendering the user as pending, an undefined value means we default
|
||||
// to 'password' as password.
|
||||
// Also we are hashing the plaintext password here if necessary
|
||||
async function handlePasswordSetup(password: string | null | undefined): Promise<string | null> {
|
||||
if (password === undefined) {
|
||||
return passwordHash;
|
||||
} else if (password === null) {
|
||||
return null;
|
||||
}
|
||||
return await hash(password, 1);
|
||||
}
|
||||
|
||||
/** Store a new user object, defaulting to a `member` */
|
||||
export async function newUser(attributes: DeepPartial<User> = {}): Promise<User> {
|
||||
const { email, password, firstName, lastName, role, lastActiveAt, ...rest } = attributes;
|
||||
return Container.get(UserRepository).create({
|
||||
email: email ?? randomEmail(),
|
||||
password: await handlePasswordSetup(password),
|
||||
firstName: firstName ?? randomName(),
|
||||
lastName: lastName ?? randomName(),
|
||||
role: role ?? GLOBAL_MEMBER_ROLE,
|
||||
lastActiveAt: lastActiveAt ?? new Date(),
|
||||
...rest,
|
||||
});
|
||||
}
|
||||
|
||||
/** Store a user object in the DB */
|
||||
export async function createUser(attributes: DeepPartial<User> = {}): Promise<User> {
|
||||
const userInstance = await newUser(attributes);
|
||||
const { user } = await Container.get(UserRepository).createUserWithProject(userInstance);
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function createLdapUser(attributes: DeepPartial<User>, ldapId: string): Promise<User> {
|
||||
const user = await createUser(attributes);
|
||||
await Container.get(AuthIdentityRepository).save(AuthIdentity.create(user, ldapId, 'ldap'));
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function createUserWithMfaEnabled(
|
||||
data: { numberOfRecoveryCodes: number } = { numberOfRecoveryCodes: 10 },
|
||||
) {
|
||||
const email = randomEmail();
|
||||
const password = randomValidPassword();
|
||||
|
||||
const toptService = new TOTPService();
|
||||
|
||||
const secret = toptService.generateSecret();
|
||||
|
||||
const mfaService = Container.get(MfaService);
|
||||
|
||||
const recoveryCodes = mfaService.generateRecoveryCodes(data.numberOfRecoveryCodes);
|
||||
|
||||
const { encryptedSecret, encryptedRecoveryCodes } = mfaService.encryptSecretAndRecoveryCodes(
|
||||
secret,
|
||||
recoveryCodes,
|
||||
);
|
||||
|
||||
const user = await createUser({
|
||||
mfaEnabled: true,
|
||||
password,
|
||||
email,
|
||||
});
|
||||
|
||||
await Container.get(UserRepository).update(user.id, {
|
||||
mfaSecret: encryptedSecret,
|
||||
mfaRecoveryCodes: encryptedRecoveryCodes,
|
||||
});
|
||||
|
||||
user.mfaSecret = encryptedSecret;
|
||||
user.mfaRecoveryCodes = encryptedRecoveryCodes;
|
||||
|
||||
return {
|
||||
user,
|
||||
rawPassword: password,
|
||||
rawSecret: secret,
|
||||
rawRecoveryCodes: recoveryCodes,
|
||||
};
|
||||
}
|
||||
|
||||
export const addApiKey = async (
|
||||
user: User,
|
||||
{ expiresAt = null, scopes = [] }: { expiresAt?: number | null; scopes?: ApiKeyScope[] } = {},
|
||||
) => {
|
||||
return await Container.get(PublicApiKeyService).createPublicApiKeyForUser(user, {
|
||||
label: randomName(),
|
||||
expiresAt,
|
||||
scopes: scopes.length ? scopes : getApiKeyScopesForRole(user),
|
||||
});
|
||||
};
|
||||
|
||||
export async function createOwnerWithApiKey({ expiresAt = null, scopes = [] }: ApiKeyOptions = {}) {
|
||||
const owner = await createOwner();
|
||||
const apiKey = await addApiKey(owner, { expiresAt, scopes });
|
||||
owner.apiKeys = [apiKey];
|
||||
return owner;
|
||||
}
|
||||
|
||||
export async function createMemberWithApiKey({
|
||||
expiresAt = null,
|
||||
scopes = [],
|
||||
}: ApiKeyOptions = {}) {
|
||||
const member = await createMember();
|
||||
const apiKey = await addApiKey(member, { expiresAt, scopes });
|
||||
member.apiKeys = [apiKey];
|
||||
return member;
|
||||
}
|
||||
|
||||
export async function createAdminWithApiKey({ expiresAt = null, scopes = [] }: ApiKeyOptions = {}) {
|
||||
const member = await createAdmin();
|
||||
const apiKey = await addApiKey(member, { expiresAt, scopes });
|
||||
member.apiKeys = [apiKey];
|
||||
return member;
|
||||
}
|
||||
|
||||
export async function createOwner() {
|
||||
return await createUser({ role: GLOBAL_OWNER_ROLE });
|
||||
}
|
||||
|
||||
export async function createMember() {
|
||||
return await createUser({ role: GLOBAL_MEMBER_ROLE });
|
||||
}
|
||||
|
||||
export async function createAdmin() {
|
||||
return await createUser({ role: GLOBAL_ADMIN_ROLE });
|
||||
}
|
||||
|
||||
export async function createChatUser() {
|
||||
return await createUser({ role: GLOBAL_CHAT_USER_ROLE });
|
||||
}
|
||||
|
||||
export async function createUserShell(role: Role): Promise<User> {
|
||||
const shell: DeepPartial<User> = { role };
|
||||
|
||||
if (role.slug !== GLOBAL_OWNER_ROLE.slug) {
|
||||
shell.email = randomEmail();
|
||||
}
|
||||
|
||||
const { user } = await Container.get(UserRepository).createUserWithProject(shell);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create many users in the DB, defaulting to a `member`.
|
||||
*/
|
||||
export async function createManyUsers(
|
||||
amount: number,
|
||||
attributes: DeepPartial<User> = {},
|
||||
): Promise<User[]> {
|
||||
const result = await Promise.all(
|
||||
Array(amount)
|
||||
.fill(0)
|
||||
.map(async () => {
|
||||
const userInstance = await newUser(attributes);
|
||||
return await Container.get(UserRepository).createUserWithProject(userInstance);
|
||||
}),
|
||||
);
|
||||
return result.map((result) => result.user);
|
||||
}
|
||||
|
||||
export const getAllUsers = async () =>
|
||||
await Container.get(UserRepository).find({
|
||||
relations: ['authIdentities', 'role'],
|
||||
});
|
||||
|
||||
export const getUserById = async (id: string) =>
|
||||
await Container.get(UserRepository).findOneOrFail({
|
||||
where: { id },
|
||||
relations: ['authIdentities', 'role'],
|
||||
});
|
||||
|
||||
export const getLdapIdentities = async () =>
|
||||
await Container.get(AuthIdentityRepository).find({
|
||||
where: { providerType: 'ldap' },
|
||||
relations: { user: true },
|
||||
});
|
||||
|
||||
export async function getGlobalOwner() {
|
||||
return await Container.get(UserRepository).findOneOrFail({
|
||||
where: { role: { slug: GLOBAL_OWNER_ROLE.slug } },
|
||||
relations: ['role'],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Project } from '@n8n/db';
|
||||
import { generateNanoId, VariablesRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { randomString } from 'n8n-workflow';
|
||||
|
||||
import { VariablesService } from '@/environments.ee/variables/variables.service.ee';
|
||||
|
||||
export async function createVariable(key = randomString(5), value = randomString(5)) {
|
||||
const result = await Container.get(VariablesRepository).save({
|
||||
id: generateNanoId(),
|
||||
key,
|
||||
value,
|
||||
});
|
||||
await Container.get(VariablesService).updateCache();
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function createProjectVariable(
|
||||
key = randomString(5),
|
||||
value = randomString(5),
|
||||
project: Project,
|
||||
) {
|
||||
const result = await Container.get(VariablesRepository).save({
|
||||
id: generateNanoId(),
|
||||
key,
|
||||
value,
|
||||
project,
|
||||
});
|
||||
|
||||
await Container.get(VariablesService).updateCache();
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getVariableByIdOrFail(id: string) {
|
||||
return await Container.get(VariablesRepository).findOneOrFail({
|
||||
where: { id },
|
||||
relations: ['project'],
|
||||
});
|
||||
}
|
||||
|
||||
export async function getVariableByKey(key: string) {
|
||||
return await Container.get(VariablesRepository).findOne({
|
||||
where: {
|
||||
key,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getVariableById(id: string) {
|
||||
return await Container.get(VariablesRepository).findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { WorkflowHistory } from '@n8n/db';
|
||||
import { WorkflowHistoryRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export async function createWorkflowHistoryItem(
|
||||
workflowId: string,
|
||||
data?: Partial<WorkflowHistory>,
|
||||
) {
|
||||
return await Container.get(WorkflowHistoryRepository).save({
|
||||
authors: 'John Smith',
|
||||
connections: {},
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1234',
|
||||
name: 'Start',
|
||||
parameters: {},
|
||||
position: [-20, 260],
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
},
|
||||
],
|
||||
versionId: uuid(),
|
||||
workflowPublishHistory: [],
|
||||
autosaved: false,
|
||||
...(data ?? {}),
|
||||
workflowId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createManyWorkflowHistoryItems(
|
||||
workflowId: string,
|
||||
count: number,
|
||||
time?: Date,
|
||||
) {
|
||||
const baseTime = (time ?? new Date()).valueOf();
|
||||
return await Promise.all(
|
||||
[...Array(count)].map(
|
||||
async (_, i) =>
|
||||
await createWorkflowHistoryItem(workflowId, {
|
||||
createdAt: new Date(baseTime + i),
|
||||
updatedAt: new Date(baseTime + i),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { WorkflowHistory, WorkflowPublishHistory } from '@n8n/db';
|
||||
import { WorkflowPublishHistoryRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
export async function createWorkflowPublishHistoryItem(
|
||||
workflowHistory: Pick<WorkflowHistory, 'workflowId' | 'versionId'>,
|
||||
data?: Partial<WorkflowPublishHistory>,
|
||||
) {
|
||||
return await Container.get(WorkflowPublishHistoryRepository).save({
|
||||
workflowId: workflowHistory.workflowId,
|
||||
versionId: workflowHistory.versionId,
|
||||
event: 'activated',
|
||||
userId: null,
|
||||
...(data ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function createManyWorkflowPublishHistoryItems(
|
||||
workflowHistory: WorkflowHistory,
|
||||
count: number,
|
||||
time?: Date,
|
||||
) {
|
||||
const baseTime = (time ?? new Date()).valueOf();
|
||||
return await Promise.all(
|
||||
[...Array(count)].map(
|
||||
async (_, i) =>
|
||||
await createWorkflowPublishHistoryItem(workflowHistory, {
|
||||
createdAt: new Date(baseTime + i),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { StatisticsNames, type WorkflowStatistics } from '@n8n/db';
|
||||
import { WorkflowStatisticsRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { Workflow } from 'n8n-workflow';
|
||||
|
||||
export async function createWorkflowStatisticsItem(
|
||||
workflowId: Workflow['id'],
|
||||
data?: Partial<WorkflowStatistics>,
|
||||
) {
|
||||
const entity = Container.get(WorkflowStatisticsRepository).create({
|
||||
count: 0,
|
||||
latestEvent: new Date().toISOString(),
|
||||
name: StatisticsNames.manualSuccess,
|
||||
...(data ?? {}),
|
||||
workflowId,
|
||||
});
|
||||
|
||||
await Container.get(WorkflowStatisticsRepository).insert(entity);
|
||||
|
||||
return entity;
|
||||
}
|
||||
Reference in New Issue
Block a user