mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-28 15:27:03 +02:00
249 lines
7.7 KiB
TypeScript
249 lines
7.7 KiB
TypeScript
import { readFileSync } from 'fs';
|
|
import type { IWorkflowBase, ExecutionSummary } from 'n8n-workflow';
|
|
import { nanoid } from 'nanoid';
|
|
|
|
// Type for execution responses from the n8n API
|
|
// Couldn't find the exact type so I put these ones together
|
|
|
|
interface ExecutionListResponse extends ExecutionSummary {
|
|
data: string;
|
|
workflowData: IWorkflowBase;
|
|
}
|
|
|
|
import type { ApiHelpers } from './api-helper';
|
|
import { TestError } from '../Types';
|
|
import { resolveFromRoot } from '../utils/path-helper';
|
|
|
|
type WorkflowImportResult = {
|
|
workflowId: string;
|
|
createdWorkflow: IWorkflowBase;
|
|
webhookPath?: string;
|
|
webhookId?: string;
|
|
};
|
|
|
|
export class WorkflowApiHelper {
|
|
constructor(private api: ApiHelpers) {}
|
|
|
|
async createWorkflow(workflow: Partial<IWorkflowBase>) {
|
|
const response = await this.api.request.post('/rest/workflows', { data: workflow });
|
|
|
|
if (!response.ok()) {
|
|
throw new TestError(`Failed to create workflow: ${await response.text()}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
return result.data ?? result;
|
|
}
|
|
|
|
/**
|
|
* Creates a workflow in a project with optional folder placement (Uses Internal API not public API)
|
|
* @param project - Required project ID where the workflow will be created
|
|
* @param options - Optional configuration for workflow creation
|
|
* @param options.folder - Optional folder ID to place the workflow in
|
|
* @param options.name - Optional workflow name. If not provided, generates a unique name using nanoid
|
|
* @returns Object containing the name and ID of the created workflow
|
|
*/
|
|
async createInProject(
|
|
project: string,
|
|
options?: {
|
|
folder?: string;
|
|
name?: string;
|
|
},
|
|
): Promise<{ name: string; id: string }> {
|
|
const workflowName = options?.name ?? `Test Workflow ${nanoid(8)}`;
|
|
|
|
const workflow = {
|
|
name: workflowName,
|
|
nodes: [],
|
|
connections: {},
|
|
settings: {},
|
|
active: false,
|
|
projectId: project,
|
|
...(options?.folder && { parentFolderId: options.folder }),
|
|
};
|
|
|
|
const response = await this.api.request.post('/rest/workflows', { data: workflow });
|
|
|
|
if (!response.ok()) {
|
|
throw new TestError(`Failed to create workflow: ${await response.text()}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
const workflowData = result.data ?? result;
|
|
|
|
return {
|
|
name: workflowName,
|
|
id: workflowData.id,
|
|
};
|
|
}
|
|
|
|
async setActive(workflowId: string, active: boolean) {
|
|
const response = await this.api.request.patch(`/rest/workflows/${workflowId}?forceSave=true`, {
|
|
data: { active },
|
|
});
|
|
|
|
if (!response.ok()) {
|
|
throw new TestError(
|
|
`Failed to ${active ? 'activate' : 'deactivate'} workflow: ${await response.text()}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make workflow unique by updating name, IDs, and webhook paths if present.
|
|
* This ensures no conflicts when importing workflows for testing.
|
|
*/
|
|
private makeWorkflowUnique(
|
|
workflow: Partial<IWorkflowBase>,
|
|
options?: { webhookPrefix?: string; idLength?: number },
|
|
) {
|
|
const idLength = options?.idLength ?? 12;
|
|
const webhookPrefix = options?.webhookPrefix ?? 'test-webhook';
|
|
const uniqueSuffix = nanoid(idLength);
|
|
|
|
// Make workflow name unique; add a default if missing
|
|
if (workflow.name && workflow.name.trim().length > 0) {
|
|
workflow.name = `${workflow.name} (Test ${uniqueSuffix})`;
|
|
} else {
|
|
workflow.name = `Test Workflow ${uniqueSuffix}`;
|
|
}
|
|
|
|
// Ensure workflow is inactive by default when not specified
|
|
workflow.active ??= false;
|
|
|
|
// Check if workflow has webhook nodes and process them
|
|
let webhookId: string | undefined;
|
|
let webhookPath: string | undefined;
|
|
|
|
if (workflow.nodes) {
|
|
for (const node of workflow.nodes) {
|
|
if (node.type === 'n8n-nodes-base.webhook') {
|
|
webhookId = nanoid(idLength);
|
|
webhookPath = `${webhookPrefix}-${webhookId}`;
|
|
node.webhookId = webhookId;
|
|
node.parameters.path = webhookPath;
|
|
}
|
|
}
|
|
}
|
|
|
|
return { webhookId, webhookPath, workflow };
|
|
}
|
|
|
|
/**
|
|
* Create a workflow from an in-memory definition, making it unique for testing.
|
|
* Returns detailed information about what was created.
|
|
*/
|
|
async createWorkflowFromDefinition(
|
|
workflow: Partial<IWorkflowBase>,
|
|
options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean },
|
|
): Promise<WorkflowImportResult> {
|
|
const { makeUnique = true, ...rest } = options ?? {};
|
|
const { webhookPath, webhookId } = makeUnique
|
|
? this.makeWorkflowUnique(workflow, rest)
|
|
: { webhookPath: undefined, webhookId: undefined };
|
|
const createdWorkflow = await this.createWorkflow(workflow);
|
|
const workflowId: string = String(createdWorkflow.id);
|
|
|
|
return {
|
|
workflowId,
|
|
createdWorkflow,
|
|
webhookPath,
|
|
webhookId,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Import a workflow from file and make it unique for testing.
|
|
* The workflow will be created with its original active state from the JSON file.
|
|
* Returns detailed information about what was imported, including webhook info if present.
|
|
*/
|
|
async importWorkflowFromFile(
|
|
fileName: string,
|
|
options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean },
|
|
): Promise<WorkflowImportResult> {
|
|
const filePath = resolveFromRoot('workflows', fileName);
|
|
const fileContent = readFileSync(filePath, 'utf8');
|
|
const workflowDefinition = JSON.parse(fileContent) as IWorkflowBase;
|
|
|
|
return await this.importWorkflowFromDefinition(workflowDefinition, options);
|
|
}
|
|
|
|
async importWorkflowFromDefinition(
|
|
workflowDefinition: Partial<IWorkflowBase>,
|
|
options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean },
|
|
): Promise<WorkflowImportResult> {
|
|
const result = await this.createWorkflowFromDefinition(workflowDefinition, options);
|
|
|
|
// Ensure the workflow is in the correct active state as specified in the JSON
|
|
if (workflowDefinition.active) {
|
|
await this.setActive(result.workflowId, workflowDefinition.active);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async getExecutions(workflowId?: string, limit = 20): Promise<ExecutionListResponse[]> {
|
|
const params = new URLSearchParams();
|
|
if (workflowId) params.set('workflowId', workflowId);
|
|
params.set('limit', limit.toString());
|
|
const response = await this.api.request.get('/rest/executions', { params });
|
|
|
|
if (!response.ok()) {
|
|
throw new TestError(`Failed to get executions: ${await response.text()}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (Array.isArray(result)) return result;
|
|
if (result.data?.results) return result.data.results;
|
|
if (result.data) return result.data;
|
|
|
|
return [];
|
|
}
|
|
|
|
async getExecution(executionId: string): Promise<ExecutionListResponse> {
|
|
const response = await this.api.request.get(`/rest/executions/${executionId}`);
|
|
|
|
if (!response.ok()) {
|
|
throw new TestError(`Failed to get execution: ${await response.text()}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
return result.data ?? result;
|
|
}
|
|
|
|
async waitForExecution(workflowId: string, timeoutMs = 10000): Promise<ExecutionListResponse> {
|
|
const initialExecutions = await this.getExecutions(workflowId, 50);
|
|
const initialCount = initialExecutions.length;
|
|
const startTime = Date.now();
|
|
|
|
while (Date.now() - startTime < timeoutMs) {
|
|
const executions = await this.getExecutions(workflowId, 50);
|
|
|
|
if (executions.length > initialCount) {
|
|
for (const execution of executions.slice(0, executions.length - initialCount)) {
|
|
if (execution.status === 'success' || execution.status === 'error') {
|
|
return execution;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const execution of executions) {
|
|
const isCompleted = execution.status === 'success' || execution.status === 'error';
|
|
if (isCompleted && execution.mode === 'webhook') {
|
|
const executionTime = new Date(
|
|
execution.startedAt ?? execution.createdAt ?? Date.now(),
|
|
).getTime();
|
|
if (executionTime >= startTime - 5000) {
|
|
return execution;
|
|
}
|
|
}
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
}
|
|
|
|
throw new TestError(`Execution did not complete within ${timeoutMs}ms`);
|
|
}
|
|
}
|