mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-06 13:33:11 +00:00
test: complete Phase 3 - comprehensive unit test coverage
- Implemented 943 unit tests across all services, parsers, and infrastructure - Created shared test utilities (test-helpers, assertions, data-generators) - Achieved high coverage for critical services: - n8n-api-client: 83.87% - workflow-diff-engine: 90.06% - node-specific-validators: 98.7% - enhanced-config-validator: 94.55% - workflow-validator: 97.59% - Added comprehensive tests for MCP tools and documentation - All tests passing in CI/CD pipeline - Integration tests deferred to separate PR due to complexity Total: 943 tests passing, ~30% overall coverage (up from 2.45%)
This commit is contained in:
302
tests/utils/test-helpers.ts
Normal file
302
tests/utils/test-helpers.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
import { vi } from 'vitest';
|
||||
import { INodeDefinition, INode, IWorkflow } from '@/types/n8n-api';
|
||||
|
||||
/**
|
||||
* Common test utilities and helpers
|
||||
*/
|
||||
|
||||
/**
|
||||
* Wait for a condition to be true
|
||||
*/
|
||||
export async function waitFor(
|
||||
condition: () => boolean | Promise<boolean>,
|
||||
options: { timeout?: number; interval?: number } = {}
|
||||
): Promise<void> {
|
||||
const { timeout = 5000, interval = 50 } = options;
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
if (await condition()) {
|
||||
return;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
}
|
||||
|
||||
throw new Error(`Timeout waiting for condition after ${timeout}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock node definition with default values
|
||||
*/
|
||||
export function createMockNodeDefinition(overrides?: Partial<INodeDefinition>): INodeDefinition {
|
||||
return {
|
||||
displayName: 'Mock Node',
|
||||
name: 'mockNode',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'A mock node for testing',
|
||||
defaults: {
|
||||
name: 'Mock Node',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [],
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock workflow node
|
||||
*/
|
||||
export function createMockNode(overrides?: Partial<INode>): INode {
|
||||
return {
|
||||
id: 'mock-node-id',
|
||||
name: 'Mock Node',
|
||||
type: 'n8n-nodes-base.mockNode',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock workflow
|
||||
*/
|
||||
export function createMockWorkflow(overrides?: Partial<IWorkflow>): IWorkflow {
|
||||
return {
|
||||
id: 'mock-workflow-id',
|
||||
name: 'Mock Workflow',
|
||||
active: false,
|
||||
nodes: [],
|
||||
connections: {},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock console methods for tests
|
||||
*/
|
||||
export function mockConsole() {
|
||||
const originalConsole = { ...console };
|
||||
|
||||
const mocks = {
|
||||
log: vi.spyOn(console, 'log').mockImplementation(() => {}),
|
||||
error: vi.spyOn(console, 'error').mockImplementation(() => {}),
|
||||
warn: vi.spyOn(console, 'warn').mockImplementation(() => {}),
|
||||
debug: vi.spyOn(console, 'debug').mockImplementation(() => {}),
|
||||
info: vi.spyOn(console, 'info').mockImplementation(() => {})
|
||||
};
|
||||
|
||||
return {
|
||||
mocks,
|
||||
restore: () => {
|
||||
Object.entries(mocks).forEach(([key, mock]) => {
|
||||
mock.mockRestore();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a deferred promise for testing async operations
|
||||
*/
|
||||
export function createDeferred<T>() {
|
||||
let resolve: (value: T) => void;
|
||||
let reject: (error: any) => void;
|
||||
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
return {
|
||||
promise,
|
||||
resolve: resolve!,
|
||||
reject: reject!
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to test error throwing
|
||||
*/
|
||||
export async function expectToThrowAsync(
|
||||
fn: () => Promise<any>,
|
||||
errorMatcher?: string | RegExp | Error
|
||||
) {
|
||||
let thrown = false;
|
||||
let error: any;
|
||||
|
||||
try {
|
||||
await fn();
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (!thrown) {
|
||||
throw new Error('Expected function to throw');
|
||||
}
|
||||
|
||||
if (errorMatcher) {
|
||||
if (typeof errorMatcher === 'string') {
|
||||
expect(error.message).toContain(errorMatcher);
|
||||
} else if (errorMatcher instanceof RegExp) {
|
||||
expect(error.message).toMatch(errorMatcher);
|
||||
} else if (errorMatcher instanceof Error) {
|
||||
expect(error).toEqual(errorMatcher);
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test database with initial data
|
||||
*/
|
||||
export function createTestDatabase(data: Record<string, any[]> = {}) {
|
||||
const db = new Map<string, any[]>();
|
||||
|
||||
// Initialize with default tables
|
||||
db.set('nodes', data.nodes || []);
|
||||
db.set('templates', data.templates || []);
|
||||
db.set('tools_documentation', data.tools_documentation || []);
|
||||
|
||||
// Add any additional tables from data
|
||||
Object.entries(data).forEach(([table, rows]) => {
|
||||
if (!db.has(table)) {
|
||||
db.set(table, rows);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
prepare: vi.fn((sql: string) => {
|
||||
const tableName = extractTableName(sql);
|
||||
const rows = db.get(tableName) || [];
|
||||
|
||||
return {
|
||||
all: vi.fn(() => rows),
|
||||
get: vi.fn((params: any) => {
|
||||
if (typeof params === 'string') {
|
||||
return rows.find((r: any) => r.id === params);
|
||||
}
|
||||
return rows[0];
|
||||
}),
|
||||
run: vi.fn((params: any) => {
|
||||
rows.push(params);
|
||||
return { changes: 1, lastInsertRowid: rows.length };
|
||||
})
|
||||
};
|
||||
}),
|
||||
exec: vi.fn(),
|
||||
close: vi.fn(),
|
||||
transaction: vi.fn((fn: Function) => fn()),
|
||||
pragma: vi.fn()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract table name from SQL query
|
||||
*/
|
||||
function extractTableName(sql: string): string {
|
||||
const patterns = [
|
||||
/FROM\s+(\w+)/i,
|
||||
/INTO\s+(\w+)/i,
|
||||
/UPDATE\s+(\w+)/i,
|
||||
/TABLE\s+(\w+)/i
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = sql.match(pattern);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return 'nodes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock HTTP response
|
||||
*/
|
||||
export function createMockResponse(data: any, status = 200) {
|
||||
return {
|
||||
data,
|
||||
status,
|
||||
statusText: status === 200 ? 'OK' : 'Error',
|
||||
headers: {},
|
||||
config: {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock HTTP error
|
||||
*/
|
||||
export function createMockHttpError(message: string, status = 500, data?: any) {
|
||||
const error: any = new Error(message);
|
||||
error.isAxiosError = true;
|
||||
error.response = {
|
||||
data: data || { message },
|
||||
status,
|
||||
statusText: status === 500 ? 'Internal Server Error' : 'Error',
|
||||
headers: {},
|
||||
config: {}
|
||||
};
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to test MCP tool calls
|
||||
*/
|
||||
export async function testMCPToolCall(
|
||||
tool: any,
|
||||
args: any,
|
||||
expectedResult?: any
|
||||
) {
|
||||
const result = await tool.handler(args);
|
||||
|
||||
if (expectedResult !== undefined) {
|
||||
expect(result).toEqual(expectedResult);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock MCP context
|
||||
*/
|
||||
export function createMockMCPContext() {
|
||||
return {
|
||||
request: vi.fn(),
|
||||
notify: vi.fn(),
|
||||
expose: vi.fn(),
|
||||
onClose: vi.fn()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot serializer for dates
|
||||
*/
|
||||
export const dateSerializer = {
|
||||
test: (value: any) => value instanceof Date,
|
||||
serialize: (value: Date) => value.toISOString()
|
||||
};
|
||||
|
||||
/**
|
||||
* Snapshot serializer for functions
|
||||
*/
|
||||
export const functionSerializer = {
|
||||
test: (value: any) => typeof value === 'function',
|
||||
serialize: () => '[Function]'
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean up test environment
|
||||
*/
|
||||
export function cleanupTestEnvironment() {
|
||||
vi.clearAllMocks();
|
||||
vi.clearAllTimers();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
Reference in New Issue
Block a user