mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
test: Add comprehensive test coverage for workflow activation/deactivation
Added 25 new tests to improve coverage for workflow activation/deactivation feature: - 7 tests for handlers-workflow-diff.test.ts (activation/deactivation handler logic) - 8 tests for workflow-diff-engine.test.ts (validate/apply activate/deactivate operations) - 10 tests for n8n-api-client.test.ts (API client activation/deactivation methods) Coverage improvements: - Branch coverage increased from 77% to 85.58% - All 3512 tests passing Tests cover: - Successful workflow activation/deactivation after updates - Error handling for activation/deactivation failures - Validation of activatable trigger nodes (webhook, schedule, etc.) - Rejection of workflows without activatable triggers - API client error cases (not found, already active/inactive, server errors) Conceived by Romuald Członkowski - www.aiadvisors.pl/en
This commit is contained in:
@@ -635,5 +635,211 @@ describe('handlers-workflow-diff', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Workflow Activation/Deactivation', () => {
|
||||||
|
it('should activate workflow after successful update', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow({ active: false });
|
||||||
|
const updatedWorkflow = { ...testWorkflow, active: false };
|
||||||
|
const activatedWorkflow = { ...testWorkflow, active: true };
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: updatedWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
shouldActivate: true,
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||||
|
mockApiClient.activateWorkflow = vi.fn().mockResolvedValue(activatedWorkflow);
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [{ type: 'activateWorkflow' }],
|
||||||
|
}, mockRepository);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data).toEqual(activatedWorkflow);
|
||||||
|
expect(result.message).toContain('Workflow activated');
|
||||||
|
expect(result.details?.active).toBe(true);
|
||||||
|
expect(mockApiClient.activateWorkflow).toHaveBeenCalledWith('test-workflow-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deactivate workflow after successful update', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow({ active: true });
|
||||||
|
const updatedWorkflow = { ...testWorkflow, active: true };
|
||||||
|
const deactivatedWorkflow = { ...testWorkflow, active: false };
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: updatedWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
shouldDeactivate: true,
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||||
|
mockApiClient.deactivateWorkflow = vi.fn().mockResolvedValue(deactivatedWorkflow);
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [{ type: 'deactivateWorkflow' }],
|
||||||
|
}, mockRepository);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data).toEqual(deactivatedWorkflow);
|
||||||
|
expect(result.message).toContain('Workflow deactivated');
|
||||||
|
expect(result.details?.active).toBe(false);
|
||||||
|
expect(mockApiClient.deactivateWorkflow).toHaveBeenCalledWith('test-workflow-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle activation failure after successful update', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow({ active: false });
|
||||||
|
const updatedWorkflow = { ...testWorkflow, active: false };
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: updatedWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
shouldActivate: true,
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||||
|
mockApiClient.activateWorkflow = vi.fn().mockRejectedValue(new Error('Activation failed: No trigger nodes'));
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [{ type: 'activateWorkflow' }],
|
||||||
|
}, mockRepository);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Workflow updated successfully but activation failed');
|
||||||
|
expect(result.details).toEqual({
|
||||||
|
workflowUpdated: true,
|
||||||
|
activationError: 'Activation failed: No trigger nodes',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle deactivation failure after successful update', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow({ active: true });
|
||||||
|
const updatedWorkflow = { ...testWorkflow, active: true };
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: updatedWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
shouldDeactivate: true,
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||||
|
mockApiClient.deactivateWorkflow = vi.fn().mockRejectedValue(new Error('Deactivation failed'));
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [{ type: 'deactivateWorkflow' }],
|
||||||
|
}, mockRepository);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Workflow updated successfully but deactivation failed');
|
||||||
|
expect(result.details).toEqual({
|
||||||
|
workflowUpdated: true,
|
||||||
|
deactivationError: 'Deactivation failed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update workflow without activation when shouldActivate is false', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow({ active: false });
|
||||||
|
const updatedWorkflow = { ...testWorkflow, active: false };
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: updatedWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
shouldActivate: false,
|
||||||
|
shouldDeactivate: false,
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||||
|
mockApiClient.activateWorkflow = vi.fn();
|
||||||
|
mockApiClient.deactivateWorkflow = vi.fn();
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [{ type: 'updateName', name: 'Updated' }],
|
||||||
|
}, mockRepository);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).not.toContain('activated');
|
||||||
|
expect(result.message).not.toContain('deactivated');
|
||||||
|
expect(mockApiClient.activateWorkflow).not.toHaveBeenCalled();
|
||||||
|
expect(mockApiClient.deactivateWorkflow).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-Error activation failures', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow({ active: false });
|
||||||
|
const updatedWorkflow = { ...testWorkflow, active: false };
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: updatedWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
shouldActivate: true,
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||||
|
mockApiClient.activateWorkflow = vi.fn().mockRejectedValue('String error');
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [{ type: 'activateWorkflow' }],
|
||||||
|
}, mockRepository);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Workflow updated successfully but activation failed');
|
||||||
|
expect(result.details).toEqual({
|
||||||
|
workflowUpdated: true,
|
||||||
|
activationError: 'Unknown error',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-Error deactivation failures', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow({ active: true });
|
||||||
|
const updatedWorkflow = { ...testWorkflow, active: true };
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: updatedWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
shouldDeactivate: true,
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||||
|
mockApiClient.deactivateWorkflow = vi.fn().mockRejectedValue({ code: 'UNKNOWN' });
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [{ type: 'deactivateWorkflow' }],
|
||||||
|
}, mockRepository);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Workflow updated successfully but deactivation failed');
|
||||||
|
expect(result.details).toEqual({
|
||||||
|
workflowUpdated: true,
|
||||||
|
deactivationError: 'Unknown error',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -362,19 +362,19 @@ describe('N8nApiClient', () => {
|
|||||||
|
|
||||||
it('should delete workflow successfully', async () => {
|
it('should delete workflow successfully', async () => {
|
||||||
mockAxiosInstance.delete.mockResolvedValue({ data: {} });
|
mockAxiosInstance.delete.mockResolvedValue({ data: {} });
|
||||||
|
|
||||||
await client.deleteWorkflow('123');
|
await client.deleteWorkflow('123');
|
||||||
|
|
||||||
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/workflows/123');
|
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/workflows/123');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle deletion error', async () => {
|
it('should handle deletion error', async () => {
|
||||||
const error = {
|
const error = {
|
||||||
message: 'Request failed',
|
message: 'Request failed',
|
||||||
response: { status: 404, data: { message: 'Not found' } }
|
response: { status: 404, data: { message: 'Not found' } }
|
||||||
};
|
};
|
||||||
await mockAxiosInstance.simulateError('delete', error);
|
await mockAxiosInstance.simulateError('delete', error);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.deleteWorkflow('123');
|
await client.deleteWorkflow('123');
|
||||||
expect.fail('Should have thrown an error');
|
expect.fail('Should have thrown an error');
|
||||||
@@ -386,6 +386,178 @@ describe('N8nApiClient', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('activateWorkflow', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new N8nApiClient(defaultConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should activate workflow successfully', async () => {
|
||||||
|
const workflow = { id: '123', name: 'Test', active: false, nodes: [], connections: {} };
|
||||||
|
const activatedWorkflow = { ...workflow, active: true };
|
||||||
|
mockAxiosInstance.post.mockResolvedValue({ data: activatedWorkflow });
|
||||||
|
|
||||||
|
const result = await client.activateWorkflow('123');
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/workflows/123/activate');
|
||||||
|
expect(result).toEqual(activatedWorkflow);
|
||||||
|
expect(result.active).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle activation error - no trigger nodes', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 400, data: { message: 'Workflow must have at least one trigger node' } }
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('post', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.activateWorkflow('123');
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nValidationError);
|
||||||
|
expect((err as N8nValidationError).message).toContain('trigger node');
|
||||||
|
expect((err as N8nValidationError).statusCode).toBe(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle activation error - workflow not found', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 404, data: { message: 'Workflow not found' } }
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('post', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.activateWorkflow('non-existent');
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nNotFoundError);
|
||||||
|
expect((err as N8nNotFoundError).message).toContain('not found');
|
||||||
|
expect((err as N8nNotFoundError).statusCode).toBe(404);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle activation error - workflow already active', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 400, data: { message: 'Workflow is already active' } }
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('post', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.activateWorkflow('123');
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nValidationError);
|
||||||
|
expect((err as N8nValidationError).message).toContain('already active');
|
||||||
|
expect((err as N8nValidationError).statusCode).toBe(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle server error during activation', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 500, data: { message: 'Internal server error' } }
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('post', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.activateWorkflow('123');
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nServerError);
|
||||||
|
expect((err as N8nServerError).message).toBe('Internal server error');
|
||||||
|
expect((err as N8nServerError).statusCode).toBe(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deactivateWorkflow', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new N8nApiClient(defaultConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deactivate workflow successfully', async () => {
|
||||||
|
const workflow = { id: '123', name: 'Test', active: true, nodes: [], connections: {} };
|
||||||
|
const deactivatedWorkflow = { ...workflow, active: false };
|
||||||
|
mockAxiosInstance.post.mockResolvedValue({ data: deactivatedWorkflow });
|
||||||
|
|
||||||
|
const result = await client.deactivateWorkflow('123');
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/workflows/123/deactivate');
|
||||||
|
expect(result).toEqual(deactivatedWorkflow);
|
||||||
|
expect(result.active).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle deactivation error - workflow not found', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 404, data: { message: 'Workflow not found' } }
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('post', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.deactivateWorkflow('non-existent');
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nNotFoundError);
|
||||||
|
expect((err as N8nNotFoundError).message).toContain('not found');
|
||||||
|
expect((err as N8nNotFoundError).statusCode).toBe(404);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle deactivation error - workflow already inactive', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 400, data: { message: 'Workflow is already inactive' } }
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('post', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.deactivateWorkflow('123');
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nValidationError);
|
||||||
|
expect((err as N8nValidationError).message).toContain('already inactive');
|
||||||
|
expect((err as N8nValidationError).statusCode).toBe(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle server error during deactivation', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 500, data: { message: 'Internal server error' } }
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('post', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.deactivateWorkflow('123');
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nServerError);
|
||||||
|
expect((err as N8nServerError).message).toBe('Internal server error');
|
||||||
|
expect((err as N8nServerError).statusCode).toBe(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle authentication error during deactivation', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 401, data: { message: 'Invalid API key' } }
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('post', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.deactivateWorkflow('123');
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nAuthenticationError);
|
||||||
|
expect((err as N8nAuthenticationError).message).toBe('Invalid API key');
|
||||||
|
expect((err as N8nAuthenticationError).statusCode).toBe(401);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('listWorkflows', () => {
|
describe('listWorkflows', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
client = new N8nApiClient(defaultConfig);
|
client = new N8nApiClient(defaultConfig);
|
||||||
|
|||||||
@@ -4269,4 +4269,354 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
expect(result.workflow.connections["When clicking 'Execute workflow'"]).toBeDefined();
|
expect(result.workflow.connections["When clicking 'Execute workflow'"]).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Workflow Activation/Deactivation Operations', () => {
|
||||||
|
it('should activate workflow with activatable trigger nodes', async () => {
|
||||||
|
// Create workflow with webhook trigger (activatable)
|
||||||
|
const workflowWithTrigger = createWorkflow('Test Workflow')
|
||||||
|
.addWebhookNode({ id: 'webhook-1', name: 'Webhook Trigger' })
|
||||||
|
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
|
||||||
|
.connect('webhook-1', 'http-1')
|
||||||
|
.build() as Workflow;
|
||||||
|
|
||||||
|
// Fix connections to use node names
|
||||||
|
const newConnections: any = {};
|
||||||
|
for (const [nodeId, outputs] of Object.entries(workflowWithTrigger.connections)) {
|
||||||
|
const node = workflowWithTrigger.nodes.find((n: any) => n.id === nodeId);
|
||||||
|
if (node) {
|
||||||
|
newConnections[node.name] = {};
|
||||||
|
for (const [outputName, connections] of Object.entries(outputs)) {
|
||||||
|
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
|
||||||
|
conns.map((conn: any) => {
|
||||||
|
const targetNode = workflowWithTrigger.nodes.find((n: any) => n.id === conn.node);
|
||||||
|
return { ...conn, node: targetNode ? targetNode.name : conn.node };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
workflowWithTrigger.connections = newConnections;
|
||||||
|
|
||||||
|
const operation: any = {
|
||||||
|
type: 'activateWorkflow'
|
||||||
|
};
|
||||||
|
|
||||||
|
const request: WorkflowDiffRequest = {
|
||||||
|
id: 'test-workflow',
|
||||||
|
operations: [operation]
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await diffEngine.applyDiff(workflowWithTrigger, request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.shouldActivate).toBe(true);
|
||||||
|
expect((result.workflow as any)._shouldActivate).toBeUndefined(); // Flag should be cleaned up
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject activation if no activatable trigger nodes', async () => {
|
||||||
|
// Create workflow with no trigger nodes at all
|
||||||
|
const workflowWithoutActivatableTrigger = createWorkflow('Test Workflow')
|
||||||
|
.addNode({
|
||||||
|
id: 'set-1',
|
||||||
|
name: 'Set Node',
|
||||||
|
type: 'n8n-nodes-base.set',
|
||||||
|
position: [100, 100],
|
||||||
|
parameters: {}
|
||||||
|
})
|
||||||
|
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
|
||||||
|
.connect('set-1', 'http-1')
|
||||||
|
.build() as Workflow;
|
||||||
|
|
||||||
|
// Fix connections to use node names
|
||||||
|
const newConnections: any = {};
|
||||||
|
for (const [nodeId, outputs] of Object.entries(workflowWithoutActivatableTrigger.connections)) {
|
||||||
|
const node = workflowWithoutActivatableTrigger.nodes.find((n: any) => n.id === nodeId);
|
||||||
|
if (node) {
|
||||||
|
newConnections[node.name] = {};
|
||||||
|
for (const [outputName, connections] of Object.entries(outputs)) {
|
||||||
|
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
|
||||||
|
conns.map((conn: any) => {
|
||||||
|
const targetNode = workflowWithoutActivatableTrigger.nodes.find((n: any) => n.id === conn.node);
|
||||||
|
return { ...conn, node: targetNode ? targetNode.name : conn.node };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
workflowWithoutActivatableTrigger.connections = newConnections;
|
||||||
|
|
||||||
|
const operation: any = {
|
||||||
|
type: 'activateWorkflow'
|
||||||
|
};
|
||||||
|
|
||||||
|
const request: WorkflowDiffRequest = {
|
||||||
|
id: 'test-workflow',
|
||||||
|
operations: [operation]
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await diffEngine.applyDiff(workflowWithoutActivatableTrigger, request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.errors).toBeDefined();
|
||||||
|
expect(result.errors![0].message).toContain('No activatable trigger nodes found');
|
||||||
|
expect(result.errors![0].message).toContain('executeWorkflowTrigger cannot activate workflows');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject activation if all trigger nodes are disabled', async () => {
|
||||||
|
// Create workflow with disabled webhook trigger
|
||||||
|
const workflowWithDisabledTrigger = createWorkflow('Test Workflow')
|
||||||
|
.addWebhookNode({ id: 'webhook-1', name: 'Webhook Trigger', disabled: true })
|
||||||
|
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
|
||||||
|
.connect('webhook-1', 'http-1')
|
||||||
|
.build() as Workflow;
|
||||||
|
|
||||||
|
// Fix connections to use node names
|
||||||
|
const newConnections: any = {};
|
||||||
|
for (const [nodeId, outputs] of Object.entries(workflowWithDisabledTrigger.connections)) {
|
||||||
|
const node = workflowWithDisabledTrigger.nodes.find((n: any) => n.id === nodeId);
|
||||||
|
if (node) {
|
||||||
|
newConnections[node.name] = {};
|
||||||
|
for (const [outputName, connections] of Object.entries(outputs)) {
|
||||||
|
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
|
||||||
|
conns.map((conn: any) => {
|
||||||
|
const targetNode = workflowWithDisabledTrigger.nodes.find((n: any) => n.id === conn.node);
|
||||||
|
return { ...conn, node: targetNode ? targetNode.name : conn.node };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
workflowWithDisabledTrigger.connections = newConnections;
|
||||||
|
|
||||||
|
const operation: any = {
|
||||||
|
type: 'activateWorkflow'
|
||||||
|
};
|
||||||
|
|
||||||
|
const request: WorkflowDiffRequest = {
|
||||||
|
id: 'test-workflow',
|
||||||
|
operations: [operation]
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await diffEngine.applyDiff(workflowWithDisabledTrigger, request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.errors).toBeDefined();
|
||||||
|
expect(result.errors![0].message).toContain('No activatable trigger nodes found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should activate workflow with schedule trigger', async () => {
|
||||||
|
// Create workflow with schedule trigger (activatable)
|
||||||
|
const workflowWithSchedule = createWorkflow('Test Workflow')
|
||||||
|
.addNode({
|
||||||
|
id: 'schedule-1',
|
||||||
|
name: 'Schedule',
|
||||||
|
type: 'n8n-nodes-base.scheduleTrigger',
|
||||||
|
position: [100, 100],
|
||||||
|
parameters: { rule: { interval: [{ field: 'hours', hoursInterval: 1 }] } }
|
||||||
|
})
|
||||||
|
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
|
||||||
|
.connect('schedule-1', 'http-1')
|
||||||
|
.build() as Workflow;
|
||||||
|
|
||||||
|
// Fix connections
|
||||||
|
const newConnections: any = {};
|
||||||
|
for (const [nodeId, outputs] of Object.entries(workflowWithSchedule.connections)) {
|
||||||
|
const node = workflowWithSchedule.nodes.find((n: any) => n.id === nodeId);
|
||||||
|
if (node) {
|
||||||
|
newConnections[node.name] = {};
|
||||||
|
for (const [outputName, connections] of Object.entries(outputs)) {
|
||||||
|
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
|
||||||
|
conns.map((conn: any) => {
|
||||||
|
const targetNode = workflowWithSchedule.nodes.find((n: any) => n.id === conn.node);
|
||||||
|
return { ...conn, node: targetNode ? targetNode.name : conn.node };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
workflowWithSchedule.connections = newConnections;
|
||||||
|
|
||||||
|
const operation: any = {
|
||||||
|
type: 'activateWorkflow'
|
||||||
|
};
|
||||||
|
|
||||||
|
const request: WorkflowDiffRequest = {
|
||||||
|
id: 'test-workflow',
|
||||||
|
operations: [operation]
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await diffEngine.applyDiff(workflowWithSchedule, request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.shouldActivate).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deactivate workflow successfully', async () => {
|
||||||
|
// Any workflow can be deactivated
|
||||||
|
const operation: any = {
|
||||||
|
type: 'deactivateWorkflow'
|
||||||
|
};
|
||||||
|
|
||||||
|
const request: WorkflowDiffRequest = {
|
||||||
|
id: 'test-workflow',
|
||||||
|
operations: [operation]
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.shouldDeactivate).toBe(true);
|
||||||
|
expect((result.workflow as any)._shouldDeactivate).toBeUndefined(); // Flag should be cleaned up
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deactivate workflow without trigger nodes', async () => {
|
||||||
|
// Create workflow without any trigger nodes
|
||||||
|
const workflowWithoutTrigger = createWorkflow('Test Workflow')
|
||||||
|
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
|
||||||
|
.addNode({
|
||||||
|
id: 'set-1',
|
||||||
|
name: 'Set',
|
||||||
|
type: 'n8n-nodes-base.set',
|
||||||
|
position: [300, 100],
|
||||||
|
parameters: {}
|
||||||
|
})
|
||||||
|
.connect('http-1', 'set-1')
|
||||||
|
.build() as Workflow;
|
||||||
|
|
||||||
|
// Fix connections
|
||||||
|
const newConnections: any = {};
|
||||||
|
for (const [nodeId, outputs] of Object.entries(workflowWithoutTrigger.connections)) {
|
||||||
|
const node = workflowWithoutTrigger.nodes.find((n: any) => n.id === nodeId);
|
||||||
|
if (node) {
|
||||||
|
newConnections[node.name] = {};
|
||||||
|
for (const [outputName, connections] of Object.entries(outputs)) {
|
||||||
|
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
|
||||||
|
conns.map((conn: any) => {
|
||||||
|
const targetNode = workflowWithoutTrigger.nodes.find((n: any) => n.id === conn.node);
|
||||||
|
return { ...conn, node: targetNode ? targetNode.name : conn.node };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
workflowWithoutTrigger.connections = newConnections;
|
||||||
|
|
||||||
|
const operation: any = {
|
||||||
|
type: 'deactivateWorkflow'
|
||||||
|
};
|
||||||
|
|
||||||
|
const request: WorkflowDiffRequest = {
|
||||||
|
id: 'test-workflow',
|
||||||
|
operations: [operation]
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await diffEngine.applyDiff(workflowWithoutTrigger, request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.shouldDeactivate).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should combine activation with other operations', async () => {
|
||||||
|
// Create workflow with webhook trigger
|
||||||
|
const workflowWithTrigger = createWorkflow('Test Workflow')
|
||||||
|
.addWebhookNode({ id: 'webhook-1', name: 'Webhook Trigger' })
|
||||||
|
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
|
||||||
|
.connect('webhook-1', 'http-1')
|
||||||
|
.build() as Workflow;
|
||||||
|
|
||||||
|
// Fix connections
|
||||||
|
const newConnections: any = {};
|
||||||
|
for (const [nodeId, outputs] of Object.entries(workflowWithTrigger.connections)) {
|
||||||
|
const node = workflowWithTrigger.nodes.find((n: any) => n.id === nodeId);
|
||||||
|
if (node) {
|
||||||
|
newConnections[node.name] = {};
|
||||||
|
for (const [outputName, connections] of Object.entries(outputs)) {
|
||||||
|
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
|
||||||
|
conns.map((conn: any) => {
|
||||||
|
const targetNode = workflowWithTrigger.nodes.find((n: any) => n.id === conn.node);
|
||||||
|
return { ...conn, node: targetNode ? targetNode.name : conn.node };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
workflowWithTrigger.connections = newConnections;
|
||||||
|
|
||||||
|
const operations: any[] = [
|
||||||
|
{
|
||||||
|
type: 'updateName',
|
||||||
|
name: 'Updated Workflow Name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'addTag',
|
||||||
|
tag: 'production'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'activateWorkflow'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const request: WorkflowDiffRequest = {
|
||||||
|
id: 'test-workflow',
|
||||||
|
operations
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await diffEngine.applyDiff(workflowWithTrigger, request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.operationsApplied).toBe(3);
|
||||||
|
expect(result.workflow!.name).toBe('Updated Workflow Name');
|
||||||
|
expect(result.workflow!.tags).toContain('production');
|
||||||
|
expect(result.shouldActivate).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject activation if workflow has executeWorkflowTrigger only', async () => {
|
||||||
|
// Create workflow with executeWorkflowTrigger (not activatable - Issue #351)
|
||||||
|
const workflowWithExecuteTrigger = createWorkflow('Test Workflow')
|
||||||
|
.addNode({
|
||||||
|
id: 'execute-1',
|
||||||
|
name: 'Execute Workflow Trigger',
|
||||||
|
type: 'n8n-nodes-base.executeWorkflowTrigger',
|
||||||
|
position: [100, 100],
|
||||||
|
parameters: {}
|
||||||
|
})
|
||||||
|
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
|
||||||
|
.connect('execute-1', 'http-1')
|
||||||
|
.build() as Workflow;
|
||||||
|
|
||||||
|
// Fix connections
|
||||||
|
const newConnections: any = {};
|
||||||
|
for (const [nodeId, outputs] of Object.entries(workflowWithExecuteTrigger.connections)) {
|
||||||
|
const node = workflowWithExecuteTrigger.nodes.find((n: any) => n.id === nodeId);
|
||||||
|
if (node) {
|
||||||
|
newConnections[node.name] = {};
|
||||||
|
for (const [outputName, connections] of Object.entries(outputs)) {
|
||||||
|
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
|
||||||
|
conns.map((conn: any) => {
|
||||||
|
const targetNode = workflowWithExecuteTrigger.nodes.find((n: any) => n.id === conn.node);
|
||||||
|
return { ...conn, node: targetNode ? targetNode.name : conn.node };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
workflowWithExecuteTrigger.connections = newConnections;
|
||||||
|
|
||||||
|
const operation: any = {
|
||||||
|
type: 'activateWorkflow'
|
||||||
|
};
|
||||||
|
|
||||||
|
const request: WorkflowDiffRequest = {
|
||||||
|
id: 'test-workflow',
|
||||||
|
operations: [operation]
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await diffEngine.applyDiff(workflowWithExecuteTrigger, request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.errors).toBeDefined();
|
||||||
|
expect(result.errors![0].message).toContain('No activatable trigger nodes found');
|
||||||
|
expect(result.errors![0].message).toContain('executeWorkflowTrigger cannot activate workflows');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user