test: add unit tests for n8n manager and workflow diff handlers
This commit is contained in:
@@ -401,7 +401,7 @@ export class WorkflowValidator {
|
|||||||
type: 'error',
|
type: 'error',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
nodeName: node.name,
|
nodeName: node.name,
|
||||||
message: error
|
message: typeof error === 'string' ? error : error.message || String(error)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -410,7 +410,7 @@ export class WorkflowValidator {
|
|||||||
type: 'warning',
|
type: 'warning',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
nodeName: node.name,
|
nodeName: node.name,
|
||||||
message: warning
|
message: typeof warning === 'string' ? warning : warning.message || String(warning)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
1094
tests/unit/mcp/handlers-n8n-manager.test.ts
Normal file
1094
tests/unit/mcp/handlers-n8n-manager.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
590
tests/unit/mcp/handlers-workflow-diff.test.ts
Normal file
590
tests/unit/mcp/handlers-workflow-diff.test.ts
Normal file
@@ -0,0 +1,590 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { handleUpdatePartialWorkflow } from '@/mcp/handlers-workflow-diff';
|
||||||
|
import { WorkflowDiffEngine } from '@/services/workflow-diff-engine';
|
||||||
|
import { N8nApiClient } from '@/services/n8n-api-client';
|
||||||
|
import {
|
||||||
|
N8nApiError,
|
||||||
|
N8nAuthenticationError,
|
||||||
|
N8nNotFoundError,
|
||||||
|
N8nValidationError,
|
||||||
|
N8nRateLimitError,
|
||||||
|
N8nServerError,
|
||||||
|
} from '@/utils/n8n-errors';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('@/services/workflow-diff-engine');
|
||||||
|
vi.mock('@/services/n8n-api-client');
|
||||||
|
vi.mock('@/config/n8n-api');
|
||||||
|
vi.mock('@/utils/logger');
|
||||||
|
vi.mock('@/mcp/handlers-n8n-manager', () => ({
|
||||||
|
getN8nApiClient: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import mocked modules
|
||||||
|
import { getN8nApiClient } from '@/mcp/handlers-n8n-manager';
|
||||||
|
import { logger } from '@/utils/logger';
|
||||||
|
|
||||||
|
describe('handlers-workflow-diff', () => {
|
||||||
|
let mockApiClient: any;
|
||||||
|
let mockDiffEngine: any;
|
||||||
|
|
||||||
|
// Helper function to create test workflow
|
||||||
|
const createTestWorkflow = (overrides = {}) => ({
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
name: 'Test Workflow',
|
||||||
|
active: true,
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'node1',
|
||||||
|
name: 'Start',
|
||||||
|
type: 'n8n-nodes-base.start',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [100, 100],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node2',
|
||||||
|
name: 'HTTP Request',
|
||||||
|
type: 'n8n-nodes-base.httpRequest',
|
||||||
|
typeVersion: 3,
|
||||||
|
position: [300, 100],
|
||||||
|
parameters: { url: 'https://api.test.com' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
node1: {
|
||||||
|
main: [[{ node: 'node2', type: 'main', index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
tags: [],
|
||||||
|
settings: {},
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
// Setup mock API client
|
||||||
|
mockApiClient = {
|
||||||
|
getWorkflow: vi.fn(),
|
||||||
|
updateWorkflow: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup mock diff engine
|
||||||
|
mockDiffEngine = {
|
||||||
|
applyDiff: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock the API client getter
|
||||||
|
vi.mocked(getN8nApiClient).mockReturnValue(mockApiClient);
|
||||||
|
|
||||||
|
// Mock WorkflowDiffEngine constructor
|
||||||
|
vi.mocked(WorkflowDiffEngine).mockImplementation(() => mockDiffEngine);
|
||||||
|
|
||||||
|
// Set up default environment
|
||||||
|
process.env.DEBUG_MCP = 'false';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleUpdatePartialWorkflow', () => {
|
||||||
|
it('should apply diff operations successfully', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
const updatedWorkflow = {
|
||||||
|
...testWorkflow,
|
||||||
|
nodes: [
|
||||||
|
...testWorkflow.nodes,
|
||||||
|
{
|
||||||
|
id: 'node3',
|
||||||
|
name: 'New Node',
|
||||||
|
type: 'n8n-nodes-base.set',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [500, 100],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const diffRequest = {
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [
|
||||||
|
{
|
||||||
|
type: 'addNode',
|
||||||
|
node: {
|
||||||
|
id: 'node3',
|
||||||
|
name: 'New Node',
|
||||||
|
type: 'n8n-nodes-base.set',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [500, 100],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: updatedWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Successfully applied 1 operation',
|
||||||
|
errors: [],
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow(diffRequest);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: updatedWorkflow,
|
||||||
|
message: 'Workflow "Test Workflow" updated successfully. Applied 1 operations.',
|
||||||
|
details: {
|
||||||
|
operationsApplied: 1,
|
||||||
|
workflowId: 'test-workflow-id',
|
||||||
|
workflowName: 'Test Workflow',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockApiClient.getWorkflow).toHaveBeenCalledWith('test-workflow-id');
|
||||||
|
expect(mockDiffEngine.applyDiff).toHaveBeenCalledWith(testWorkflow, diffRequest);
|
||||||
|
expect(mockApiClient.updateWorkflow).toHaveBeenCalledWith('test-workflow-id', updatedWorkflow);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle validation-only mode', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
const diffRequest = {
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [
|
||||||
|
{
|
||||||
|
type: 'updateNode',
|
||||||
|
nodeId: 'node2',
|
||||||
|
changes: { name: 'Updated HTTP Request' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
validateOnly: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: testWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Validation successful',
|
||||||
|
errors: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow(diffRequest);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
message: 'Validation successful',
|
||||||
|
data: {
|
||||||
|
valid: true,
|
||||||
|
operationsToApply: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockApiClient.updateWorkflow).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple operations', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
const diffRequest = {
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [
|
||||||
|
{
|
||||||
|
type: 'updateNode',
|
||||||
|
nodeId: 'node1',
|
||||||
|
changes: { name: 'Updated Start' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'addNode',
|
||||||
|
node: {
|
||||||
|
id: 'node3',
|
||||||
|
name: 'Set Node',
|
||||||
|
type: 'n8n-nodes-base.set',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [500, 100],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'addConnection',
|
||||||
|
source: 'node2',
|
||||||
|
target: 'node3',
|
||||||
|
sourceOutput: 'main',
|
||||||
|
targetInput: 'main',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: { ...testWorkflow, nodes: [...testWorkflow.nodes, {}] },
|
||||||
|
operationsApplied: 3,
|
||||||
|
message: 'Successfully applied 3 operations',
|
||||||
|
errors: [],
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue({ ...testWorkflow });
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow(diffRequest);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).toContain('Applied 3 operations');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle diff application failures', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
const diffRequest = {
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [
|
||||||
|
{
|
||||||
|
type: 'updateNode',
|
||||||
|
nodeId: 'non-existent-node',
|
||||||
|
changes: { name: 'Updated' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: false,
|
||||||
|
workflow: null,
|
||||||
|
operationsApplied: 0,
|
||||||
|
message: 'Failed to apply operations',
|
||||||
|
errors: ['Node "non-existent-node" not found'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow(diffRequest);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to apply diff operations',
|
||||||
|
details: {
|
||||||
|
errors: ['Node "non-existent-node" not found'],
|
||||||
|
operationsApplied: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockApiClient.updateWorkflow).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle API not configured error', async () => {
|
||||||
|
vi.mocked(getN8nApiClient).mockReturnValue(null);
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-id',
|
||||||
|
operations: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: 'n8n API not configured. Please set N8N_API_URL and N8N_API_KEY environment variables.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle workflow not found error', async () => {
|
||||||
|
const notFoundError = new N8nNotFoundError('Workflow not found');
|
||||||
|
mockApiClient.getWorkflow.mockRejectedValue(notFoundError);
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'non-existent',
|
||||||
|
operations: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: 'Workflow not found',
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle API errors during update', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
const validationError = new N8nValidationError('Invalid workflow structure', {
|
||||||
|
field: 'connections',
|
||||||
|
message: 'Invalid connection configuration',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: testWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockRejectedValue(validationError);
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-id',
|
||||||
|
operations: [{ type: 'updateNode', nodeId: 'node1', changes: {} }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid workflow structure',
|
||||||
|
code: 'VALIDATION_ERROR',
|
||||||
|
details: {
|
||||||
|
field: 'connections',
|
||||||
|
message: 'Invalid connection configuration',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle input validation errors', async () => {
|
||||||
|
const invalidInput = {
|
||||||
|
id: 'test-id',
|
||||||
|
operations: [
|
||||||
|
{
|
||||||
|
// Missing required 'type' field
|
||||||
|
nodeId: 'node1',
|
||||||
|
changes: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow(invalidInput);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Invalid input');
|
||||||
|
expect(result.details).toHaveProperty('errors');
|
||||||
|
expect(result.details.errors).toBeInstanceOf(Array);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex operation types', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
const diffRequest = {
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [
|
||||||
|
{
|
||||||
|
type: 'moveNode',
|
||||||
|
nodeId: 'node2',
|
||||||
|
position: [400, 200],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'removeConnection',
|
||||||
|
source: 'node1',
|
||||||
|
target: 'node2',
|
||||||
|
sourceOutput: 'main',
|
||||||
|
targetInput: 'main',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'updateSettings',
|
||||||
|
settings: {
|
||||||
|
executionOrder: 'v1',
|
||||||
|
timezone: 'America/New_York',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'addTag',
|
||||||
|
tag: 'automated',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: { ...testWorkflow, settings: { executionOrder: 'v1' } },
|
||||||
|
operationsApplied: 4,
|
||||||
|
message: 'Successfully applied 4 operations',
|
||||||
|
errors: [],
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue({ ...testWorkflow });
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow(diffRequest);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(mockDiffEngine.applyDiff).toHaveBeenCalledWith(testWorkflow, diffRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle debug logging when enabled', async () => {
|
||||||
|
process.env.DEBUG_MCP = 'true';
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: testWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
|
||||||
|
await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-id',
|
||||||
|
operations: [{ type: 'updateNode', nodeId: 'node1', changes: {} }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(logger.debug).toHaveBeenCalledWith(
|
||||||
|
'Workflow diff request received',
|
||||||
|
expect.objectContaining({
|
||||||
|
argsType: 'object',
|
||||||
|
operationCount: 1,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle generic errors', async () => {
|
||||||
|
const genericError = new Error('Something went wrong');
|
||||||
|
mockApiClient.getWorkflow.mockRejectedValue(genericError);
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-id',
|
||||||
|
operations: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: 'Something went wrong',
|
||||||
|
});
|
||||||
|
expect(logger.error).toHaveBeenCalledWith('Failed to update partial workflow', genericError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle authentication errors', async () => {
|
||||||
|
const authError = new N8nAuthenticationError('Invalid API key');
|
||||||
|
mockApiClient.getWorkflow.mockRejectedValue(authError);
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-id',
|
||||||
|
operations: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid API key',
|
||||||
|
code: 'AUTHENTICATION_ERROR',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle rate limit errors', async () => {
|
||||||
|
const rateLimitError = new N8nRateLimitError('Too many requests', 60);
|
||||||
|
mockApiClient.getWorkflow.mockRejectedValue(rateLimitError);
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-id',
|
||||||
|
operations: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: 'Too many requests',
|
||||||
|
code: 'RATE_LIMIT_ERROR',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle server errors', async () => {
|
||||||
|
const serverError = new N8nServerError('Internal server error');
|
||||||
|
mockApiClient.getWorkflow.mockRejectedValue(serverError);
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-id',
|
||||||
|
operations: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: 'Internal server error',
|
||||||
|
code: 'SERVER_ERROR',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate operation structure', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
const diffRequest = {
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [
|
||||||
|
{
|
||||||
|
type: 'updateNode',
|
||||||
|
nodeId: 'node1',
|
||||||
|
nodeName: 'Start', // Both nodeId and nodeName provided
|
||||||
|
changes: { name: 'New Start' },
|
||||||
|
description: 'Update start node name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'addConnection',
|
||||||
|
source: 'node1',
|
||||||
|
target: 'node2',
|
||||||
|
sourceOutput: 'main',
|
||||||
|
targetInput: 'main',
|
||||||
|
sourceIndex: 0,
|
||||||
|
targetIndex: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: testWorkflow,
|
||||||
|
operationsApplied: 2,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow(diffRequest);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(mockDiffEngine.applyDiff).toHaveBeenCalledWith(testWorkflow, diffRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty operations array', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
const diffRequest = {
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: testWorkflow,
|
||||||
|
operationsApplied: 0,
|
||||||
|
message: 'No operations to apply',
|
||||||
|
errors: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow(diffRequest);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).toContain('Applied 0 operations');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle partial diff application', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
const diffRequest = {
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [
|
||||||
|
{ type: 'updateNode', nodeId: 'node1', changes: { name: 'Updated' } },
|
||||||
|
{ type: 'updateNode', nodeId: 'invalid-node', changes: { name: 'Fail' } },
|
||||||
|
{ type: 'addTag', tag: 'test' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: false,
|
||||||
|
workflow: null,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Partially applied operations',
|
||||||
|
errors: ['Operation 2 failed: Node "invalid-node" not found'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow(diffRequest);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to apply diff operations',
|
||||||
|
details: {
|
||||||
|
errors: ['Operation 2 failed: Node "invalid-node" not found'],
|
||||||
|
operationsApplied: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { EnhancedConfigValidator, ValidationMode, ValidationProfile } from '@/services/enhanced-config-validator';
|
import { EnhancedConfigValidator, ValidationMode, ValidationProfile } from '@/services/enhanced-config-validator';
|
||||||
|
import { ValidationError } from '@/services/config-validator';
|
||||||
import { NodeSpecificValidators } from '@/services/node-specific-validators';
|
import { NodeSpecificValidators } from '@/services/node-specific-validators';
|
||||||
import { nodeFactory } from '@tests/fixtures/factories/node.factory';
|
import { nodeFactory } from '@tests/fixtures/factories/node.factory';
|
||||||
|
|
||||||
@@ -197,7 +198,7 @@ describe('EnhancedConfigValidator', () => {
|
|||||||
{ type: 'invalid_type', property: 'channel', message: 'Different type error' }
|
{ type: 'invalid_type', property: 'channel', message: 'Different type error' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const deduplicated = EnhancedConfigValidator['deduplicateErrors'](errors);
|
const deduplicated = EnhancedConfigValidator['deduplicateErrors'](errors as ValidationError[]);
|
||||||
|
|
||||||
expect(deduplicated).toHaveLength(2);
|
expect(deduplicated).toHaveLength(2);
|
||||||
// Should keep the longer message
|
// Should keep the longer message
|
||||||
@@ -210,7 +211,7 @@ describe('EnhancedConfigValidator', () => {
|
|||||||
{ type: 'missing_required', property: 'url', message: 'URL is required', fix: 'Add a valid URL like https://api.example.com' }
|
{ type: 'missing_required', property: 'url', message: 'URL is required', fix: 'Add a valid URL like https://api.example.com' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const deduplicated = EnhancedConfigValidator['deduplicateErrors'](errors);
|
const deduplicated = EnhancedConfigValidator['deduplicateErrors'](errors as ValidationError[]);
|
||||||
|
|
||||||
expect(deduplicated).toHaveLength(1);
|
expect(deduplicated).toHaveLength(1);
|
||||||
expect(deduplicated[0].fix).toBeDefined();
|
expect(deduplicated[0].fix).toBeDefined();
|
||||||
@@ -575,7 +576,7 @@ describe('EnhancedConfigValidator', () => {
|
|||||||
const mockValidateMongoDB = vi.mocked(NodeSpecificValidators.validateMongoDB);
|
const mockValidateMongoDB = vi.mocked(NodeSpecificValidators.validateMongoDB);
|
||||||
|
|
||||||
const config = { collection: 'users', operation: 'insert' };
|
const config = { collection: 'users', operation: 'insert' };
|
||||||
const properties = [];
|
const properties: any[] = [];
|
||||||
|
|
||||||
const result = EnhancedConfigValidator.validateWithMode(
|
const result = EnhancedConfigValidator.validateWithMode(
|
||||||
'nodes-base.mongoDb',
|
'nodes-base.mongoDb',
|
||||||
@@ -593,7 +594,7 @@ describe('EnhancedConfigValidator', () => {
|
|||||||
const mockValidateMySQL = vi.mocked(NodeSpecificValidators.validateMySQL);
|
const mockValidateMySQL = vi.mocked(NodeSpecificValidators.validateMySQL);
|
||||||
|
|
||||||
const config = { table: 'users', operation: 'insert' };
|
const config = { table: 'users', operation: 'insert' };
|
||||||
const properties = [];
|
const properties: any[] = [];
|
||||||
|
|
||||||
const result = EnhancedConfigValidator.validateWithMode(
|
const result = EnhancedConfigValidator.validateWithMode(
|
||||||
'nodes-base.mysql',
|
'nodes-base.mysql',
|
||||||
@@ -609,7 +610,7 @@ describe('EnhancedConfigValidator', () => {
|
|||||||
const mockValidatePostgres = vi.mocked(NodeSpecificValidators.validatePostgres);
|
const mockValidatePostgres = vi.mocked(NodeSpecificValidators.validatePostgres);
|
||||||
|
|
||||||
const config = { table: 'users', operation: 'select' };
|
const config = { table: 'users', operation: 'select' };
|
||||||
const properties = [];
|
const properties: any[] = [];
|
||||||
|
|
||||||
const result = EnhancedConfigValidator.validateWithMode(
|
const result = EnhancedConfigValidator.validateWithMode(
|
||||||
'nodes-base.postgres',
|
'nodes-base.postgres',
|
||||||
@@ -666,7 +667,7 @@ describe('EnhancedConfigValidator', () => {
|
|||||||
|
|
||||||
// Mock isPropertyVisible to return false for hidden property
|
// Mock isPropertyVisible to return false for hidden property
|
||||||
const isVisibleSpy = vi.spyOn(EnhancedConfigValidator as any, 'isPropertyVisible');
|
const isVisibleSpy = vi.spyOn(EnhancedConfigValidator as any, 'isPropertyVisible');
|
||||||
isVisibleSpy.mockImplementation((prop) => prop.name !== 'hidden');
|
isVisibleSpy.mockImplementation((prop: any) => prop.name !== 'hidden');
|
||||||
|
|
||||||
const result = EnhancedConfigValidator.validateWithMode(
|
const result = EnhancedConfigValidator.validateWithMode(
|
||||||
'nodes-base.test',
|
'nodes-base.test',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { N8nApiClient, N8nApiClientConfig } from '../../../src/services/n8n-api-client';
|
import { N8nApiClient, N8nApiClientConfig } from '../../../src/services/n8n-api-client';
|
||||||
|
import { ExecutionStatus } from '../../../src/types/n8n-api';
|
||||||
import {
|
import {
|
||||||
N8nApiError,
|
N8nApiError,
|
||||||
N8nAuthenticationError,
|
N8nAuthenticationError,
|
||||||
@@ -242,8 +243,8 @@ describe('N8nApiClient', () => {
|
|||||||
expect.fail('Should have thrown an error');
|
expect.fail('Should have thrown an error');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err).toBeInstanceOf(N8nValidationError);
|
expect(err).toBeInstanceOf(N8nValidationError);
|
||||||
expect(err.message).toBe('Invalid workflow');
|
expect((err as N8nValidationError).message).toBe('Invalid workflow');
|
||||||
expect(err.statusCode).toBe(400);
|
expect((err as N8nValidationError).statusCode).toBe(400);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -275,8 +276,8 @@ describe('N8nApiClient', () => {
|
|||||||
expect.fail('Should have thrown an error');
|
expect.fail('Should have thrown an error');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err).toBeInstanceOf(N8nNotFoundError);
|
expect(err).toBeInstanceOf(N8nNotFoundError);
|
||||||
expect(err.message).toContain('not found');
|
expect((err as N8nNotFoundError).message).toContain('not found');
|
||||||
expect(err.statusCode).toBe(404);
|
expect((err as N8nNotFoundError).statusCode).toBe(404);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -327,8 +328,8 @@ describe('N8nApiClient', () => {
|
|||||||
expect.fail('Should have thrown an error');
|
expect.fail('Should have thrown an error');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err).toBeInstanceOf(N8nValidationError);
|
expect(err).toBeInstanceOf(N8nValidationError);
|
||||||
expect(err.message).toBe('Invalid update');
|
expect((err as N8nValidationError).message).toBe('Invalid update');
|
||||||
expect(err.statusCode).toBe(400);
|
expect((err as N8nValidationError).statusCode).toBe(400);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -358,8 +359,8 @@ describe('N8nApiClient', () => {
|
|||||||
expect.fail('Should have thrown an error');
|
expect.fail('Should have thrown an error');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err).toBeInstanceOf(N8nNotFoundError);
|
expect(err).toBeInstanceOf(N8nNotFoundError);
|
||||||
expect(err.message).toContain('not found');
|
expect((err as N8nNotFoundError).message).toContain('not found');
|
||||||
expect(err.statusCode).toBe(404);
|
expect((err as N8nNotFoundError).statusCode).toBe(404);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -427,7 +428,7 @@ describe('N8nApiClient', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should list executions with filters', async () => {
|
it('should list executions with filters', async () => {
|
||||||
const params = { workflowId: '123', status: 'success', limit: 50 };
|
const params = { workflowId: '123', status: ExecutionStatus.SUCCESS, limit: 50 };
|
||||||
const response = { data: [], nextCursor: null };
|
const response = { data: [], nextCursor: null };
|
||||||
mockAxiosInstance.get.mockResolvedValue({ data: response });
|
mockAxiosInstance.get.mockResolvedValue({ data: response });
|
||||||
|
|
||||||
@@ -560,8 +561,8 @@ describe('N8nApiClient', () => {
|
|||||||
expect.fail('Should have thrown an error');
|
expect.fail('Should have thrown an error');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err).toBeInstanceOf(N8nAuthenticationError);
|
expect(err).toBeInstanceOf(N8nAuthenticationError);
|
||||||
expect(err.message).toBe('Invalid API key');
|
expect((err as N8nAuthenticationError).message).toBe('Invalid API key');
|
||||||
expect(err.statusCode).toBe(401);
|
expect((err as N8nAuthenticationError).statusCode).toBe(401);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -581,9 +582,9 @@ describe('N8nApiClient', () => {
|
|||||||
expect.fail('Should have thrown an error');
|
expect.fail('Should have thrown an error');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err).toBeInstanceOf(N8nRateLimitError);
|
expect(err).toBeInstanceOf(N8nRateLimitError);
|
||||||
expect(err.message).toContain('Rate limit exceeded');
|
expect((err as N8nRateLimitError).message).toContain('Rate limit exceeded');
|
||||||
expect(err.statusCode).toBe(429);
|
expect((err as N8nRateLimitError).statusCode).toBe(429);
|
||||||
expect(err.details?.retryAfter).toBe(60);
|
expect(((err as N8nRateLimitError).details as any)?.retryAfter).toBe(60);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -602,8 +603,8 @@ describe('N8nApiClient', () => {
|
|||||||
expect.fail('Should have thrown an error');
|
expect.fail('Should have thrown an error');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err).toBeInstanceOf(N8nServerError);
|
expect(err).toBeInstanceOf(N8nServerError);
|
||||||
expect(err.message).toBe('Internal server error');
|
expect((err as N8nServerError).message).toBe('Internal server error');
|
||||||
expect(err.statusCode).toBe(500);
|
expect((err as N8nServerError).statusCode).toBe(500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -827,12 +828,12 @@ describe('N8nApiClient', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Capture the interceptor functions
|
// Capture the interceptor functions
|
||||||
vi.mocked(mockAxiosInstance.interceptors.request.use).mockImplementation((onFulfilled) => {
|
vi.mocked(mockAxiosInstance.interceptors.request.use).mockImplementation((onFulfilled: any) => {
|
||||||
requestInterceptor = onFulfilled;
|
requestInterceptor = onFulfilled;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mocked(mockAxiosInstance.interceptors.response.use).mockImplementation((onFulfilled, onRejected) => {
|
vi.mocked(mockAxiosInstance.interceptors.response.use).mockImplementation((onFulfilled: any, onRejected: any) => {
|
||||||
responseInterceptor = onFulfilled;
|
responseInterceptor = onFulfilled;
|
||||||
responseErrorInterceptor = onRejected;
|
responseErrorInterceptor = onRejected;
|
||||||
return 0;
|
return 0;
|
||||||
@@ -882,7 +883,7 @@ describe('N8nApiClient', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await responseErrorInterceptor(error).catch(e => e);
|
const result = await responseErrorInterceptor(error).catch((e: any) => e);
|
||||||
expect(result).toBeInstanceOf(N8nValidationError);
|
expect(result).toBeInstanceOf(N8nValidationError);
|
||||||
expect(result.message).toBe('Bad request');
|
expect(result.message).toBe('Bad request');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ describe('n8n-validation', () => {
|
|||||||
tags: ['tag1'],
|
tags: ['tag1'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleaned = cleanWorkflowForCreate(workflow);
|
const cleaned = cleanWorkflowForCreate(workflow as any);
|
||||||
|
|
||||||
expect(cleaned).not.toHaveProperty('id');
|
expect(cleaned).not.toHaveProperty('id');
|
||||||
expect(cleaned).not.toHaveProperty('createdAt');
|
expect(cleaned).not.toHaveProperty('createdAt');
|
||||||
@@ -281,7 +281,7 @@ describe('n8n-validation', () => {
|
|||||||
connections: {},
|
connections: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleaned = cleanWorkflowForCreate(workflow);
|
const cleaned = cleanWorkflowForCreate(workflow as Workflow);
|
||||||
expect(cleaned.settings).toEqual(defaultWorkflowSettings);
|
expect(cleaned.settings).toEqual(defaultWorkflowSettings);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -298,7 +298,7 @@ describe('n8n-validation', () => {
|
|||||||
settings: customSettings,
|
settings: customSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleaned = cleanWorkflowForCreate(workflow);
|
const cleaned = cleanWorkflowForCreate(workflow as Workflow);
|
||||||
expect(cleaned.settings).toEqual(customSettings);
|
expect(cleaned.settings).toEqual(customSettings);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -370,7 +370,7 @@ describe('n8n-validation', () => {
|
|||||||
.connect('Webhook', 'Send Slack')
|
.connect('Webhook', 'Send Slack')
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
const errors = validateWorkflowStructure(workflow);
|
const errors = validateWorkflowStructure(workflow as any);
|
||||||
expect(errors).toEqual([]);
|
expect(errors).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -380,7 +380,7 @@ describe('n8n-validation', () => {
|
|||||||
connections: {},
|
connections: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const errors = validateWorkflowStructure(workflow);
|
const errors = validateWorkflowStructure(workflow as any);
|
||||||
expect(errors).toContain('Workflow name is required');
|
expect(errors).toContain('Workflow name is required');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -390,7 +390,7 @@ describe('n8n-validation', () => {
|
|||||||
connections: {},
|
connections: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const errors = validateWorkflowStructure(workflow);
|
const errors = validateWorkflowStructure(workflow as any);
|
||||||
expect(errors).toContain('Workflow must have at least one node');
|
expect(errors).toContain('Workflow must have at least one node');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -401,17 +401,17 @@ describe('n8n-validation', () => {
|
|||||||
connections: {},
|
connections: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const errors = validateWorkflowStructure(workflow);
|
const errors = validateWorkflowStructure(workflow as any);
|
||||||
expect(errors).toContain('Workflow must have at least one node');
|
expect(errors).toContain('Workflow must have at least one node');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect missing connections', () => {
|
it('should detect missing connections', () => {
|
||||||
const workflow = {
|
const workflow = {
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
nodes: [{ id: 'node-1', name: 'Node 1', type: 'n8n-nodes-base.set', typeVersion: 1, position: [0, 0], parameters: {} }],
|
nodes: [{ id: 'node-1', name: 'Node 1', type: 'n8n-nodes-base.set', typeVersion: 1, position: [0, 0] as [number, number], parameters: {} }],
|
||||||
};
|
};
|
||||||
|
|
||||||
const errors = validateWorkflowStructure(workflow);
|
const errors = validateWorkflowStructure(workflow as any);
|
||||||
expect(errors).toContain('Workflow connections are required');
|
expect(errors).toContain('Workflow connections are required');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -429,7 +429,7 @@ describe('n8n-validation', () => {
|
|||||||
connections: {},
|
connections: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const errors = validateWorkflowStructure(workflow);
|
const errors = validateWorkflowStructure(workflow as any);
|
||||||
expect(errors).toEqual([]);
|
expect(errors).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1155,7 +1155,7 @@ describe('n8n-validation', () => {
|
|||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
const errors = validateWorkflowStructure(workflow);
|
const errors = validateWorkflowStructure(workflow as any);
|
||||||
expect(errors).toEqual([]);
|
expect(errors).toEqual([]);
|
||||||
|
|
||||||
// Validate individual components
|
// Validate individual components
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
// Convert connections from ID-based to name-based (as n8n expects)
|
// Convert connections from ID-based to name-based (as n8n expects)
|
||||||
const newConnections: any = {};
|
const newConnections: any = {};
|
||||||
for (const [nodeId, outputs] of Object.entries(baseWorkflow.connections)) {
|
for (const [nodeId, outputs] of Object.entries(baseWorkflow.connections)) {
|
||||||
const node = baseWorkflow.nodes.find(n => n.id === nodeId);
|
const node = baseWorkflow.nodes.find((n: any) => n.id === nodeId);
|
||||||
if (node) {
|
if (node) {
|
||||||
newConnections[node.name] = {};
|
newConnections[node.name] = {};
|
||||||
for (const [outputName, connections] of Object.entries(outputs)) {
|
for (const [outputName, connections] of Object.entries(outputs)) {
|
||||||
newConnections[node.name][outputName] = (connections as any[]).map(conns =>
|
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
|
||||||
conns.map(conn => {
|
conns.map((conn: any) => {
|
||||||
const targetNode = baseWorkflow.nodes.find(n => n.id === conn.node);
|
const targetNode = baseWorkflow.nodes.find((n: any) => n.id === conn.node);
|
||||||
return {
|
return {
|
||||||
...conn,
|
...conn,
|
||||||
node: targetNode ? targetNode.name : conn.node
|
node: targetNode ? targetNode.name : conn.node
|
||||||
@@ -62,7 +62,7 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
|
|
||||||
describe('Operation Limits', () => {
|
describe('Operation Limits', () => {
|
||||||
it('should reject more than 5 operations', async () => {
|
it('should reject more than 5 operations', async () => {
|
||||||
const operations = Array(6).fill(null).map((_, i) => ({
|
const operations = Array(6).fill(null).map((_: any, i: number) => ({
|
||||||
type: 'updateName',
|
type: 'updateName',
|
||||||
name: `Name ${i}`
|
name: `Name ${i}`
|
||||||
} as UpdateNameOperation));
|
} as UpdateNameOperation));
|
||||||
@@ -213,7 +213,7 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.workflow!.nodes).toHaveLength(2);
|
expect(result.workflow!.nodes).toHaveLength(2);
|
||||||
expect(result.workflow!.nodes.find(n => n.id === 'http-1')).toBeUndefined();
|
expect(result.workflow!.nodes.find((n: any) => n.id === 'http-1')).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove node by name', async () => {
|
it('should remove node by name', async () => {
|
||||||
@@ -231,7 +231,7 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.workflow!.nodes).toHaveLength(2);
|
expect(result.workflow!.nodes).toHaveLength(2);
|
||||||
expect(result.workflow!.nodes.find(n => n.name === 'HTTP Request')).toBeUndefined();
|
expect(result.workflow!.nodes.find((n: any) => n.name === 'HTTP Request')).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clean up connections when removing node', async () => {
|
it('should clean up connections when removing node', async () => {
|
||||||
@@ -295,7 +295,7 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
const updatedNode = result.workflow!.nodes.find(n => n.id === 'http-1');
|
const updatedNode = result.workflow!.nodes.find((n: any) => n.id === 'http-1');
|
||||||
expect(updatedNode!.parameters.method).toBe('POST');
|
expect(updatedNode!.parameters.method).toBe('POST');
|
||||||
expect(updatedNode!.parameters.url).toBe('https://new-api.example.com');
|
expect(updatedNode!.parameters.url).toBe('https://new-api.example.com');
|
||||||
});
|
});
|
||||||
@@ -319,7 +319,7 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
const updatedNode = result.workflow!.nodes.find(n => n.name === 'Slack');
|
const updatedNode = result.workflow!.nodes.find((n: any) => n.name === 'Slack');
|
||||||
expect(updatedNode!.parameters.resource).toBe('channel');
|
expect(updatedNode!.parameters.resource).toBe('channel');
|
||||||
expect(updatedNode!.parameters.operation).toBe('create');
|
expect(updatedNode!.parameters.operation).toBe('create');
|
||||||
expect((updatedNode!.credentials as any).slackApi.name).toBe('New Slack Account');
|
expect((updatedNode!.credentials as any).slackApi.name).toBe('New Slack Account');
|
||||||
@@ -362,7 +362,7 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
const movedNode = result.workflow!.nodes.find(n => n.id === 'http-1');
|
const movedNode = result.workflow!.nodes.find((n: any) => n.id === 'http-1');
|
||||||
expect(movedNode!.position).toEqual([1000, 500]);
|
expect(movedNode!.position).toEqual([1000, 500]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -381,7 +381,7 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
const movedNode = result.workflow!.nodes.find(n => n.name === 'Webhook');
|
const movedNode = result.workflow!.nodes.find((n: any) => n.name === 'Webhook');
|
||||||
expect(movedNode!.position).toEqual([100, 100]);
|
expect(movedNode!.position).toEqual([100, 100]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -401,7 +401,7 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
const disabledNode = result.workflow!.nodes.find(n => n.id === 'http-1');
|
const disabledNode = result.workflow!.nodes.find((n: any) => n.id === 'http-1');
|
||||||
expect(disabledNode!.disabled).toBe(true);
|
expect(disabledNode!.disabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -422,7 +422,7 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
const enabledNode = result.workflow!.nodes.find(n => n.id === 'http-1');
|
const enabledNode = result.workflow!.nodes.find((n: any) => n.id === 'http-1');
|
||||||
expect(enabledNode!.disabled).toBe(false);
|
expect(enabledNode!.disabled).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1052,7 +1052,7 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
const updatedNode = result.workflow!.nodes.find(n => n.name === 'Webhook');
|
const updatedNode = result.workflow!.nodes.find((n: any) => n.name === 'Webhook');
|
||||||
expect(updatedNode!.parameters.path).toBe('new-webhook-path');
|
expect(updatedNode!.parameters.path).toBe('new-webhook-path');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { NodeRepository } from '@/database/node-repository';
|
|||||||
import { EnhancedConfigValidator } from '@/services/enhanced-config-validator';
|
import { EnhancedConfigValidator } from '@/services/enhanced-config-validator';
|
||||||
import { ExpressionValidator } from '@/services/expression-validator';
|
import { ExpressionValidator } from '@/services/expression-validator';
|
||||||
import { createWorkflow } from '@tests/utils/builders/workflow.builder';
|
import { createWorkflow } from '@tests/utils/builders/workflow.builder';
|
||||||
import type { WorkflowNode, WorkflowJson } from '@/services/workflow-validator';
|
import type { WorkflowNode, Workflow } from '@/types/n8n-api';
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock('@/database/node-repository');
|
vi.mock('@/database/node-repository');
|
||||||
@@ -21,7 +21,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
// Create mock instances
|
// Create mock instances
|
||||||
mockNodeRepository = new NodeRepository() as any;
|
mockNodeRepository = new NodeRepository({} as any) as any;
|
||||||
mockEnhancedConfigValidator = EnhancedConfigValidator as any;
|
mockEnhancedConfigValidator = EnhancedConfigValidator as any;
|
||||||
|
|
||||||
// Set up default mock behaviors
|
// Set up default mock behaviors
|
||||||
@@ -131,15 +131,19 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
|||||||
vi.mocked(mockEnhancedConfigValidator.validateWithMode).mockReturnValue({
|
vi.mocked(mockEnhancedConfigValidator.validateWithMode).mockReturnValue({
|
||||||
errors: [],
|
errors: [],
|
||||||
warnings: [],
|
warnings: [],
|
||||||
suggestions: []
|
suggestions: [],
|
||||||
});
|
mode: 'operation' as const,
|
||||||
|
valid: true,
|
||||||
|
visibleProperties: [],
|
||||||
|
hiddenProperties: []
|
||||||
|
} as any);
|
||||||
|
|
||||||
vi.mocked(ExpressionValidator.validateNodeExpressions).mockReturnValue({
|
vi.mocked(ExpressionValidator.validateNodeExpressions).mockReturnValue({
|
||||||
valid: true,
|
valid: true,
|
||||||
errors: [],
|
errors: [],
|
||||||
warnings: [],
|
warnings: [],
|
||||||
usedVariables: new Set(),
|
usedVariables: new Set(),
|
||||||
referencedNodes: new Set()
|
usedNodes: new Set()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create validator instance
|
// Create validator instance
|
||||||
@@ -637,10 +641,14 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
|||||||
|
|
||||||
it('should add node validation errors and warnings', async () => {
|
it('should add node validation errors and warnings', async () => {
|
||||||
vi.mocked(mockEnhancedConfigValidator.validateWithMode).mockReturnValue({
|
vi.mocked(mockEnhancedConfigValidator.validateWithMode).mockReturnValue({
|
||||||
errors: ['Missing required field: url'],
|
errors: [{ type: 'missing_required', property: 'url', message: 'Missing required field: url' }],
|
||||||
warnings: ['Consider using HTTPS'],
|
warnings: [{ type: 'security', property: 'url', message: 'Consider using HTTPS' }],
|
||||||
suggestions: []
|
suggestions: [],
|
||||||
});
|
mode: 'operation' as const,
|
||||||
|
valid: false,
|
||||||
|
visibleProperties: [],
|
||||||
|
hiddenProperties: []
|
||||||
|
} as any);
|
||||||
|
|
||||||
const workflow = {
|
const workflow = {
|
||||||
nodes: [
|
nodes: [
|
||||||
@@ -658,8 +666,8 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
|||||||
|
|
||||||
const result = await validator.validateWorkflow(workflow);
|
const result = await validator.validateWorkflow(workflow);
|
||||||
|
|
||||||
expect(result.errors.some(e => e.message === 'Missing required field: url')).toBe(true);
|
expect(result.errors.some(e => e.message.includes('Missing required field: url'))).toBe(true);
|
||||||
expect(result.warnings.some(w => w.message === 'Consider using HTTPS')).toBe(true);
|
expect(result.warnings.some(w => w.message.includes('Consider using HTTPS'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle node validation failures gracefully', async () => {
|
it('should handle node validation failures gracefully', async () => {
|
||||||
@@ -1120,7 +1128,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
|||||||
errors: ['Invalid expression syntax'],
|
errors: ['Invalid expression syntax'],
|
||||||
warnings: ['Deprecated variable usage'],
|
warnings: ['Deprecated variable usage'],
|
||||||
usedVariables: new Set(['$json']),
|
usedVariables: new Set(['$json']),
|
||||||
referencedNodes: new Set()
|
usedNodes: new Set()
|
||||||
});
|
});
|
||||||
|
|
||||||
const workflow = {
|
const workflow = {
|
||||||
|
|||||||
Reference in New Issue
Block a user