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:
BIN
data/nodes.db
BIN
data/nodes.db
Binary file not shown.
39
package-lock.json
generated
39
package-lock.json
generated
@@ -32,6 +32,7 @@
|
|||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
|
"axios-mock-adapter": "^2.1.0",
|
||||||
"fishery": "^2.3.1",
|
"fishery": "^2.3.1",
|
||||||
"msw": "^2.10.4",
|
"msw": "^2.10.4",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
@@ -15101,6 +15102,44 @@
|
|||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios-mock-adapter": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"is-buffer": "^2.0.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"axios": ">= 0.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axios-mock-adapter/node_modules/is-buffer": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axios-retry": {
|
"node_modules/axios-retry": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz",
|
||||||
|
|||||||
@@ -98,6 +98,7 @@
|
|||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
|
"axios-mock-adapter": "^2.1.0",
|
||||||
"fishery": "^2.3.1",
|
"fishery": "^2.3.1",
|
||||||
"msw": "^2.10.4",
|
"msw": "^2.10.4",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
||||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
||||||
import { spawn } from 'child_process';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Integration test for AI Agent node extraction
|
|
||||||
* This simulates an MCP client requesting the AI Agent code from n8n
|
|
||||||
*/
|
|
||||||
async function testAIAgentExtraction() {
|
|
||||||
console.log('=== AI Agent Node Extraction Test ===\n');
|
|
||||||
|
|
||||||
// Create MCP client
|
|
||||||
const client = new Client(
|
|
||||||
{
|
|
||||||
name: 'test-mcp-client',
|
|
||||||
version: '1.0.0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
capabilities: {},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('1. Starting MCP server...');
|
|
||||||
const serverPath = path.join(__dirname, '../../dist/index.js');
|
|
||||||
const transport = new StdioClientTransport({
|
|
||||||
command: 'node',
|
|
||||||
args: [serverPath],
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
N8N_API_URL: process.env.N8N_API_URL || 'http://localhost:5678',
|
|
||||||
N8N_API_KEY: process.env.N8N_API_KEY || 'test-key',
|
|
||||||
LOG_LEVEL: 'debug',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.connect(transport);
|
|
||||||
console.log('✓ Connected to MCP server\n');
|
|
||||||
|
|
||||||
// Test 1: List available tools
|
|
||||||
console.log('2. Listing available tools...');
|
|
||||||
const toolsResponse = await client.request(
|
|
||||||
{ method: 'tools/list' },
|
|
||||||
{} as any
|
|
||||||
) as any;
|
|
||||||
console.log(`✓ Found ${toolsResponse.tools.length} tools`);
|
|
||||||
|
|
||||||
const hasNodeSourceTool = toolsResponse.tools.some(
|
|
||||||
(tool: any) => tool.name === 'get_node_source_code'
|
|
||||||
);
|
|
||||||
console.log(`✓ Node source extraction tool available: ${hasNodeSourceTool}\n`);
|
|
||||||
|
|
||||||
// Test 2: List available nodes
|
|
||||||
console.log('3. Listing available nodes...');
|
|
||||||
const listNodesResponse = await client.request(
|
|
||||||
{
|
|
||||||
method: 'tools/call',
|
|
||||||
params: {
|
|
||||||
name: 'list_available_nodes',
|
|
||||||
arguments: {
|
|
||||||
search: 'agent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{} as any
|
|
||||||
) as any;
|
|
||||||
console.log(`✓ Found nodes matching 'agent':`);
|
|
||||||
const content = JSON.parse(listNodesResponse.content[0].text);
|
|
||||||
content.nodes.forEach((node: any) => {
|
|
||||||
console.log(` - ${node.displayName}: ${node.description}`);
|
|
||||||
});
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
// Test 3: Extract AI Agent node source code
|
|
||||||
console.log('4. Extracting AI Agent node source code...');
|
|
||||||
const aiAgentResponse = await client.request(
|
|
||||||
{
|
|
||||||
method: 'tools/call',
|
|
||||||
params: {
|
|
||||||
name: 'get_node_source_code',
|
|
||||||
arguments: {
|
|
||||||
nodeType: '@n8n/n8n-nodes-langchain.Agent',
|
|
||||||
includeCredentials: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{} as any
|
|
||||||
) as any;
|
|
||||||
|
|
||||||
const result = JSON.parse(aiAgentResponse.content[0].text);
|
|
||||||
console.log('✓ Successfully extracted AI Agent node:');
|
|
||||||
console.log(` - Node Type: ${result.nodeType}`);
|
|
||||||
console.log(` - Location: ${result.location}`);
|
|
||||||
console.log(` - Source Code Length: ${result.sourceCode.length} characters`);
|
|
||||||
console.log(` - Has Credential Code: ${!!result.credentialCode}`);
|
|
||||||
|
|
||||||
if (result.packageInfo) {
|
|
||||||
console.log(` - Package: ${result.packageInfo.name} v${result.packageInfo.version}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show a snippet of the code
|
|
||||||
console.log('\n5. Source Code Preview:');
|
|
||||||
console.log('```javascript');
|
|
||||||
console.log(result.sourceCode.substring(0, 500) + '...');
|
|
||||||
console.log('```\n');
|
|
||||||
|
|
||||||
// Test 4: Use resource endpoint
|
|
||||||
console.log('6. Testing resource endpoint...');
|
|
||||||
const resourceResponse = await client.request(
|
|
||||||
{
|
|
||||||
method: 'resources/read',
|
|
||||||
params: {
|
|
||||||
uri: 'nodes://source/@n8n/n8n-nodes-langchain.Agent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{} as any
|
|
||||||
) as any;
|
|
||||||
console.log('✓ Successfully read node source via resource endpoint\n');
|
|
||||||
|
|
||||||
console.log('=== Test Completed Successfully ===');
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
process.exit(0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Test failed:', error);
|
|
||||||
await client.close();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
testAIAgentExtraction().catch(console.error);
|
|
||||||
280
tests/utils/assertions.ts
Normal file
280
tests/utils/assertions.ts
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
import { expect } from 'vitest';
|
||||||
|
import { INodeDefinition, IWorkflow, INode } from '@/types/n8n-api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom assertions for n8n-mcp tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that a value is a valid node definition
|
||||||
|
*/
|
||||||
|
export function expectValidNodeDefinition(node: any): asserts node is INodeDefinition {
|
||||||
|
expect(node).toBeDefined();
|
||||||
|
expect(node).toHaveProperty('name');
|
||||||
|
expect(node).toHaveProperty('displayName');
|
||||||
|
expect(node).toHaveProperty('version');
|
||||||
|
expect(node).toHaveProperty('properties');
|
||||||
|
expect(node.properties).toBeInstanceOf(Array);
|
||||||
|
|
||||||
|
// Check version is a positive number
|
||||||
|
expect(node.version).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Check required string fields
|
||||||
|
expect(typeof node.name).toBe('string');
|
||||||
|
expect(typeof node.displayName).toBe('string');
|
||||||
|
expect(node.name).not.toBe('');
|
||||||
|
expect(node.displayName).not.toBe('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that a value is a valid workflow
|
||||||
|
*/
|
||||||
|
export function expectValidWorkflow(workflow: any): asserts workflow is IWorkflow {
|
||||||
|
expect(workflow).toBeDefined();
|
||||||
|
expect(workflow).toHaveProperty('nodes');
|
||||||
|
expect(workflow).toHaveProperty('connections');
|
||||||
|
expect(workflow.nodes).toBeInstanceOf(Array);
|
||||||
|
expect(workflow.connections).toBeTypeOf('object');
|
||||||
|
|
||||||
|
// Check each node is valid
|
||||||
|
workflow.nodes.forEach((node: any) => {
|
||||||
|
expectValidWorkflowNode(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check connections reference valid nodes
|
||||||
|
const nodeIds = new Set(workflow.nodes.map((n: INode) => n.id));
|
||||||
|
Object.keys(workflow.connections).forEach(sourceId => {
|
||||||
|
expect(nodeIds.has(sourceId)).toBe(true);
|
||||||
|
|
||||||
|
const connections = workflow.connections[sourceId];
|
||||||
|
Object.values(connections).forEach((outputConnections: any) => {
|
||||||
|
outputConnections.forEach((connectionSet: any) => {
|
||||||
|
connectionSet.forEach((connection: any) => {
|
||||||
|
expect(nodeIds.has(connection.node)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that a value is a valid workflow node
|
||||||
|
*/
|
||||||
|
export function expectValidWorkflowNode(node: any): asserts node is INode {
|
||||||
|
expect(node).toBeDefined();
|
||||||
|
expect(node).toHaveProperty('id');
|
||||||
|
expect(node).toHaveProperty('name');
|
||||||
|
expect(node).toHaveProperty('type');
|
||||||
|
expect(node).toHaveProperty('typeVersion');
|
||||||
|
expect(node).toHaveProperty('position');
|
||||||
|
expect(node).toHaveProperty('parameters');
|
||||||
|
|
||||||
|
// Check types
|
||||||
|
expect(typeof node.id).toBe('string');
|
||||||
|
expect(typeof node.name).toBe('string');
|
||||||
|
expect(typeof node.type).toBe('string');
|
||||||
|
expect(typeof node.typeVersion).toBe('number');
|
||||||
|
expect(node.position).toBeInstanceOf(Array);
|
||||||
|
expect(node.position).toHaveLength(2);
|
||||||
|
expect(typeof node.position[0]).toBe('number');
|
||||||
|
expect(typeof node.position[1]).toBe('number');
|
||||||
|
expect(node.parameters).toBeTypeOf('object');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that validation errors contain expected messages
|
||||||
|
*/
|
||||||
|
export function expectValidationErrors(errors: any[], expectedMessages: string[]) {
|
||||||
|
expect(errors).toHaveLength(expectedMessages.length);
|
||||||
|
|
||||||
|
const errorMessages = errors.map(e =>
|
||||||
|
typeof e === 'string' ? e : e.message || e.error || String(e)
|
||||||
|
);
|
||||||
|
|
||||||
|
expectedMessages.forEach(expected => {
|
||||||
|
const found = errorMessages.some(msg =>
|
||||||
|
msg.toLowerCase().includes(expected.toLowerCase())
|
||||||
|
);
|
||||||
|
expect(found).toBe(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that a property definition is valid
|
||||||
|
*/
|
||||||
|
export function expectValidPropertyDefinition(property: any) {
|
||||||
|
expect(property).toBeDefined();
|
||||||
|
expect(property).toHaveProperty('name');
|
||||||
|
expect(property).toHaveProperty('displayName');
|
||||||
|
expect(property).toHaveProperty('type');
|
||||||
|
|
||||||
|
// Check required fields
|
||||||
|
expect(typeof property.name).toBe('string');
|
||||||
|
expect(typeof property.displayName).toBe('string');
|
||||||
|
expect(typeof property.type).toBe('string');
|
||||||
|
|
||||||
|
// Check common property types
|
||||||
|
const validTypes = [
|
||||||
|
'string', 'number', 'boolean', 'options', 'multiOptions',
|
||||||
|
'collection', 'fixedCollection', 'json', 'color', 'dateTime'
|
||||||
|
];
|
||||||
|
expect(validTypes).toContain(property.type);
|
||||||
|
|
||||||
|
// Check options if present
|
||||||
|
if (property.type === 'options' || property.type === 'multiOptions') {
|
||||||
|
expect(property.options).toBeInstanceOf(Array);
|
||||||
|
expect(property.options.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
property.options.forEach((option: any) => {
|
||||||
|
expect(option).toHaveProperty('name');
|
||||||
|
expect(option).toHaveProperty('value');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check displayOptions if present
|
||||||
|
if (property.displayOptions) {
|
||||||
|
expect(property.displayOptions).toBeTypeOf('object');
|
||||||
|
if (property.displayOptions.show) {
|
||||||
|
expect(property.displayOptions.show).toBeTypeOf('object');
|
||||||
|
}
|
||||||
|
if (property.displayOptions.hide) {
|
||||||
|
expect(property.displayOptions.hide).toBeTypeOf('object');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that an MCP tool response is valid
|
||||||
|
*/
|
||||||
|
export function expectValidMCPResponse(response: any) {
|
||||||
|
expect(response).toBeDefined();
|
||||||
|
|
||||||
|
// Check for error response
|
||||||
|
if (response.error) {
|
||||||
|
expect(response.error).toHaveProperty('code');
|
||||||
|
expect(response.error).toHaveProperty('message');
|
||||||
|
expect(typeof response.error.code).toBe('number');
|
||||||
|
expect(typeof response.error.message).toBe('string');
|
||||||
|
} else {
|
||||||
|
// Check for success response
|
||||||
|
expect(response.result).toBeDefined();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that a database row has required metadata
|
||||||
|
*/
|
||||||
|
export function expectDatabaseMetadata(row: any) {
|
||||||
|
expect(row).toHaveProperty('created_at');
|
||||||
|
expect(row).toHaveProperty('updated_at');
|
||||||
|
|
||||||
|
// Check dates are valid
|
||||||
|
const createdAt = new Date(row.created_at);
|
||||||
|
const updatedAt = new Date(row.updated_at);
|
||||||
|
|
||||||
|
expect(createdAt.toString()).not.toBe('Invalid Date');
|
||||||
|
expect(updatedAt.toString()).not.toBe('Invalid Date');
|
||||||
|
expect(updatedAt.getTime()).toBeGreaterThanOrEqual(createdAt.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that an expression is valid n8n expression syntax
|
||||||
|
*/
|
||||||
|
export function expectValidExpression(expression: string) {
|
||||||
|
// Check for basic expression syntax
|
||||||
|
const expressionPattern = /\{\{.*\}\}/;
|
||||||
|
expect(expression).toMatch(expressionPattern);
|
||||||
|
|
||||||
|
// Check for balanced braces
|
||||||
|
let braceCount = 0;
|
||||||
|
for (const char of expression) {
|
||||||
|
if (char === '{') braceCount++;
|
||||||
|
if (char === '}') braceCount--;
|
||||||
|
expect(braceCount).toBeGreaterThanOrEqual(0);
|
||||||
|
}
|
||||||
|
expect(braceCount).toBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that a template is valid
|
||||||
|
*/
|
||||||
|
export function expectValidTemplate(template: any) {
|
||||||
|
expect(template).toBeDefined();
|
||||||
|
expect(template).toHaveProperty('id');
|
||||||
|
expect(template).toHaveProperty('name');
|
||||||
|
expect(template).toHaveProperty('workflow');
|
||||||
|
expect(template).toHaveProperty('categories');
|
||||||
|
|
||||||
|
// Check workflow is valid
|
||||||
|
expectValidWorkflow(template.workflow);
|
||||||
|
|
||||||
|
// Check categories
|
||||||
|
expect(template.categories).toBeInstanceOf(Array);
|
||||||
|
expect(template.categories.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that search results are relevant
|
||||||
|
*/
|
||||||
|
export function expectRelevantSearchResults(
|
||||||
|
results: any[],
|
||||||
|
query: string,
|
||||||
|
minRelevance = 0.5
|
||||||
|
) {
|
||||||
|
expect(results).toBeInstanceOf(Array);
|
||||||
|
|
||||||
|
if (results.length === 0) return;
|
||||||
|
|
||||||
|
// Check each result contains query terms
|
||||||
|
const queryTerms = query.toLowerCase().split(/\s+/);
|
||||||
|
|
||||||
|
results.forEach(result => {
|
||||||
|
const searchableText = JSON.stringify(result).toLowerCase();
|
||||||
|
const matchCount = queryTerms.filter(term =>
|
||||||
|
searchableText.includes(term)
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const relevance = matchCount / queryTerms.length;
|
||||||
|
expect(relevance).toBeGreaterThanOrEqual(minRelevance);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom matchers for n8n-mcp
|
||||||
|
*/
|
||||||
|
export const customMatchers = {
|
||||||
|
toBeValidNodeDefinition(received: any) {
|
||||||
|
try {
|
||||||
|
expectValidNodeDefinition(received);
|
||||||
|
return { pass: true, message: () => 'Node definition is valid' };
|
||||||
|
} catch (error: any) {
|
||||||
|
return { pass: false, message: () => error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toBeValidWorkflow(received: any) {
|
||||||
|
try {
|
||||||
|
expectValidWorkflow(received);
|
||||||
|
return { pass: true, message: () => 'Workflow is valid' };
|
||||||
|
} catch (error: any) {
|
||||||
|
return { pass: false, message: () => error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toContainValidationError(received: any[], expected: string) {
|
||||||
|
const errorMessages = received.map(e =>
|
||||||
|
typeof e === 'string' ? e : e.message || e.error || String(e)
|
||||||
|
);
|
||||||
|
|
||||||
|
const found = errorMessages.some(msg =>
|
||||||
|
msg.toLowerCase().includes(expected.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass: found,
|
||||||
|
message: () => found
|
||||||
|
? `Found validation error containing "${expected}"`
|
||||||
|
: `No validation error found containing "${expected}". Errors: ${errorMessages.join(', ')}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
353
tests/utils/data-generators.ts
Normal file
353
tests/utils/data-generators.ts
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { INodeDefinition, INode, IWorkflow } from '@/types/n8n-api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data generators for creating realistic test data
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random node type
|
||||||
|
*/
|
||||||
|
export function generateNodeType(): string {
|
||||||
|
const packages = ['n8n-nodes-base', '@n8n/n8n-nodes-langchain'];
|
||||||
|
const nodeTypes = [
|
||||||
|
'webhook', 'httpRequest', 'slack', 'googleSheets', 'postgres',
|
||||||
|
'function', 'code', 'if', 'switch', 'merge', 'splitInBatches',
|
||||||
|
'emailSend', 'redis', 'mongodb', 'mysql', 'ftp', 'ssh'
|
||||||
|
];
|
||||||
|
|
||||||
|
const pkg = faker.helpers.arrayElement(packages);
|
||||||
|
const type = faker.helpers.arrayElement(nodeTypes);
|
||||||
|
|
||||||
|
return `${pkg}.${type}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate property definitions for a node
|
||||||
|
*/
|
||||||
|
export function generateProperties(count = 5): any[] {
|
||||||
|
const properties = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const type = faker.helpers.arrayElement([
|
||||||
|
'string', 'number', 'boolean', 'options', 'collection'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const property: any = {
|
||||||
|
displayName: faker.helpers.arrayElement([
|
||||||
|
'Resource', 'Operation', 'Field', 'Value', 'Method',
|
||||||
|
'URL', 'Headers', 'Body', 'Authentication', 'Options'
|
||||||
|
]),
|
||||||
|
name: faker.helpers.slugify(faker.word.noun()).toLowerCase(),
|
||||||
|
type,
|
||||||
|
default: generateDefaultValue(type),
|
||||||
|
description: faker.lorem.sentence()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === 'options') {
|
||||||
|
property.options = generateOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faker.datatype.boolean()) {
|
||||||
|
property.required = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faker.datatype.boolean()) {
|
||||||
|
property.displayOptions = generateDisplayOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.push(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate default value based on type
|
||||||
|
*/
|
||||||
|
function generateDefaultValue(type: string): any {
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
return faker.lorem.word();
|
||||||
|
case 'number':
|
||||||
|
return faker.number.int({ min: 0, max: 100 });
|
||||||
|
case 'boolean':
|
||||||
|
return faker.datatype.boolean();
|
||||||
|
case 'options':
|
||||||
|
return 'option1';
|
||||||
|
case 'collection':
|
||||||
|
return {};
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate options for select fields
|
||||||
|
*/
|
||||||
|
function generateOptions(count = 3): any[] {
|
||||||
|
const options = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
options.push({
|
||||||
|
name: faker.helpers.arrayElement([
|
||||||
|
'Create', 'Read', 'Update', 'Delete', 'List',
|
||||||
|
'Get', 'Post', 'Put', 'Patch', 'Send'
|
||||||
|
]),
|
||||||
|
value: `option${i + 1}`,
|
||||||
|
description: faker.lorem.sentence()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate display options for conditional fields
|
||||||
|
*/
|
||||||
|
function generateDisplayOptions(): any {
|
||||||
|
return {
|
||||||
|
show: {
|
||||||
|
resource: [faker.helpers.arrayElement(['user', 'post', 'message'])],
|
||||||
|
operation: [faker.helpers.arrayElement(['create', 'update', 'get'])]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a complete node definition
|
||||||
|
*/
|
||||||
|
export function generateNodeDefinition(overrides?: Partial<INodeDefinition>): INodeDefinition {
|
||||||
|
const nodeCategory = faker.helpers.arrayElement([
|
||||||
|
'Core Nodes', 'Communication', 'Data Transformation',
|
||||||
|
'Development', 'Files', 'Productivity', 'Analytics'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayName: faker.company.name() + ' Node',
|
||||||
|
name: faker.helpers.slugify(faker.company.name()).toLowerCase(),
|
||||||
|
group: [faker.helpers.arrayElement(['trigger', 'transform', 'output'])],
|
||||||
|
version: faker.number.float({ min: 1, max: 3, fractionDigits: 1 }),
|
||||||
|
subtitle: `={{$parameter["operation"] + ": " + $parameter["resource"]}}`,
|
||||||
|
description: faker.lorem.paragraph(),
|
||||||
|
defaults: {
|
||||||
|
name: faker.company.name(),
|
||||||
|
color: faker.color.rgb()
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
credentials: faker.datatype.boolean() ? [{
|
||||||
|
name: faker.helpers.slugify(faker.company.name()).toLowerCase() + 'Api',
|
||||||
|
required: true
|
||||||
|
}] : undefined,
|
||||||
|
properties: generateProperties(),
|
||||||
|
codex: {
|
||||||
|
categories: [nodeCategory],
|
||||||
|
subcategories: {
|
||||||
|
[nodeCategory]: [faker.word.noun()]
|
||||||
|
},
|
||||||
|
alias: [faker.word.noun(), faker.word.verb()]
|
||||||
|
},
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate workflow nodes
|
||||||
|
*/
|
||||||
|
export function generateWorkflowNodes(count = 3): INode[] {
|
||||||
|
const nodes: INode[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
nodes.push({
|
||||||
|
id: faker.string.uuid(),
|
||||||
|
name: faker.helpers.arrayElement([
|
||||||
|
'Webhook', 'HTTP Request', 'Set', 'Function', 'IF',
|
||||||
|
'Slack', 'Email', 'Database', 'Code'
|
||||||
|
]) + (i > 0 ? i : ''),
|
||||||
|
type: generateNodeType(),
|
||||||
|
typeVersion: faker.number.float({ min: 1, max: 3, fractionDigits: 1 }),
|
||||||
|
position: [
|
||||||
|
250 + i * 200,
|
||||||
|
300 + (i % 2) * 100
|
||||||
|
],
|
||||||
|
parameters: generateNodeParameters()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate node parameters
|
||||||
|
*/
|
||||||
|
function generateNodeParameters(): Record<string, any> {
|
||||||
|
const params: Record<string, any> = {};
|
||||||
|
|
||||||
|
// Common parameters
|
||||||
|
if (faker.datatype.boolean()) {
|
||||||
|
params.resource = faker.helpers.arrayElement(['user', 'post', 'message']);
|
||||||
|
params.operation = faker.helpers.arrayElement(['create', 'get', 'update', 'delete']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type-specific parameters
|
||||||
|
if (faker.datatype.boolean()) {
|
||||||
|
params.url = faker.internet.url();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faker.datatype.boolean()) {
|
||||||
|
params.method = faker.helpers.arrayElement(['GET', 'POST', 'PUT', 'DELETE']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faker.datatype.boolean()) {
|
||||||
|
params.authentication = faker.helpers.arrayElement(['none', 'basicAuth', 'oAuth2']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some random parameters
|
||||||
|
const randomParamCount = faker.number.int({ min: 1, max: 5 });
|
||||||
|
for (let i = 0; i < randomParamCount; i++) {
|
||||||
|
const key = faker.word.noun().toLowerCase();
|
||||||
|
params[key] = faker.helpers.arrayElement([
|
||||||
|
faker.lorem.word(),
|
||||||
|
faker.number.int(),
|
||||||
|
faker.datatype.boolean(),
|
||||||
|
'={{ $json.data }}'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate workflow connections
|
||||||
|
*/
|
||||||
|
export function generateConnections(nodes: INode[]): Record<string, any> {
|
||||||
|
const connections: Record<string, any> = {};
|
||||||
|
|
||||||
|
// Connect nodes sequentially
|
||||||
|
for (let i = 0; i < nodes.length - 1; i++) {
|
||||||
|
const sourceId = nodes[i].id;
|
||||||
|
const targetId = nodes[i + 1].id;
|
||||||
|
|
||||||
|
if (!connections[sourceId]) {
|
||||||
|
connections[sourceId] = { main: [[]] };
|
||||||
|
}
|
||||||
|
|
||||||
|
connections[sourceId].main[0].push({
|
||||||
|
node: targetId,
|
||||||
|
type: 'main',
|
||||||
|
index: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some random connections
|
||||||
|
if (nodes.length > 2 && faker.datatype.boolean()) {
|
||||||
|
const sourceIdx = faker.number.int({ min: 0, max: nodes.length - 2 });
|
||||||
|
const targetIdx = faker.number.int({ min: sourceIdx + 1, max: nodes.length - 1 });
|
||||||
|
|
||||||
|
const sourceId = nodes[sourceIdx].id;
|
||||||
|
const targetId = nodes[targetIdx].id;
|
||||||
|
|
||||||
|
if (connections[sourceId]?.main[0]) {
|
||||||
|
connections[sourceId].main[0].push({
|
||||||
|
node: targetId,
|
||||||
|
type: 'main',
|
||||||
|
index: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return connections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a complete workflow
|
||||||
|
*/
|
||||||
|
export function generateWorkflow(nodeCount = 3): IWorkflow {
|
||||||
|
const nodes = generateWorkflowNodes(nodeCount);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: faker.string.uuid(),
|
||||||
|
name: faker.helpers.arrayElement([
|
||||||
|
'Data Processing Workflow',
|
||||||
|
'API Integration Flow',
|
||||||
|
'Notification Pipeline',
|
||||||
|
'ETL Process',
|
||||||
|
'Webhook Handler'
|
||||||
|
]),
|
||||||
|
active: faker.datatype.boolean(),
|
||||||
|
nodes,
|
||||||
|
connections: generateConnections(nodes),
|
||||||
|
settings: {
|
||||||
|
executionOrder: 'v1',
|
||||||
|
saveManualExecutions: true,
|
||||||
|
callerPolicy: 'workflowsFromSameOwner',
|
||||||
|
timezone: faker.location.timeZone()
|
||||||
|
},
|
||||||
|
staticData: {},
|
||||||
|
tags: generateTags(),
|
||||||
|
createdAt: faker.date.past().toISOString(),
|
||||||
|
updatedAt: faker.date.recent().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate workflow tags
|
||||||
|
*/
|
||||||
|
function generateTags(): Array<{ id: string; name: string }> {
|
||||||
|
const tagCount = faker.number.int({ min: 0, max: 3 });
|
||||||
|
const tags = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < tagCount; i++) {
|
||||||
|
tags.push({
|
||||||
|
id: faker.string.uuid(),
|
||||||
|
name: faker.helpers.arrayElement([
|
||||||
|
'production', 'development', 'testing',
|
||||||
|
'automation', 'integration', 'notification'
|
||||||
|
])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate test templates
|
||||||
|
*/
|
||||||
|
export function generateTemplate() {
|
||||||
|
const workflow = generateWorkflow();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: faker.number.int({ min: 1000, max: 9999 }),
|
||||||
|
name: workflow.name,
|
||||||
|
description: faker.lorem.paragraph(),
|
||||||
|
workflow,
|
||||||
|
categories: faker.helpers.arrayElements([
|
||||||
|
'Sales', 'Marketing', 'Engineering',
|
||||||
|
'HR', 'Finance', 'Operations'
|
||||||
|
], { min: 1, max: 3 }),
|
||||||
|
useCases: faker.helpers.arrayElements([
|
||||||
|
'Lead Generation', 'Data Sync', 'Notifications',
|
||||||
|
'Reporting', 'Automation', 'Integration'
|
||||||
|
], { min: 1, max: 3 }),
|
||||||
|
views: faker.number.int({ min: 0, max: 10000 }),
|
||||||
|
recentViews: faker.number.int({ min: 0, max: 100 })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate bulk test data
|
||||||
|
*/
|
||||||
|
export function generateBulkData(counts: {
|
||||||
|
nodes?: number;
|
||||||
|
workflows?: number;
|
||||||
|
templates?: number;
|
||||||
|
}) {
|
||||||
|
const { nodes = 10, workflows = 5, templates = 3 } = counts;
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes: Array.from({ length: nodes }, () => generateNodeDefinition()),
|
||||||
|
workflows: Array.from({ length: workflows }, () => generateWorkflow()),
|
||||||
|
templates: Array.from({ length: templates }, () => generateTemplate())
|
||||||
|
};
|
||||||
|
}
|
||||||
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