mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
fix: add edge case handling and test coverage for schema-based validation
- Add defensive null checks for malformed schema data in config-validator.ts - Improve mode extraction logic with better type safety and filtering - Add 4 comprehensive test cases: * Array format modes handling * Malformed schema graceful degradation * Empty modes object handling * Missing typeOptions skip validation - Add database schema coverage audit script - Document schema coverage: 21.4% of resourceLocator nodes have modes defined Coverage impact: - 15 nodes with complete schemas: strict validation - 55 nodes without schemas: graceful degradation (no false positives) All tests passing: 99 tests (33 resourceLocator, 21 edge cases, 26 node-specific, 19 security) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
78
scripts/audit-schema-coverage.ts
Normal file
78
scripts/audit-schema-coverage.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Database Schema Coverage Audit Script
|
||||||
|
*
|
||||||
|
* Audits the database to determine how many nodes have complete schema information
|
||||||
|
* for resourceLocator mode validation. This helps assess the coverage of our
|
||||||
|
* schema-driven validation approach.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const dbPath = path.join(__dirname, '../data/nodes.db');
|
||||||
|
const db = new Database(dbPath, { readonly: true });
|
||||||
|
|
||||||
|
console.log('=== Schema Coverage Audit ===\n');
|
||||||
|
|
||||||
|
// Query 1: How many nodes have resourceLocator properties?
|
||||||
|
const totalResourceLocator = db.prepare(`
|
||||||
|
SELECT COUNT(*) as count FROM nodes
|
||||||
|
WHERE properties_schema LIKE '%resourceLocator%'
|
||||||
|
`).get() as { count: number };
|
||||||
|
|
||||||
|
console.log(`Nodes with resourceLocator properties: ${totalResourceLocator.count}`);
|
||||||
|
|
||||||
|
// Query 2: Of those, how many have modes defined?
|
||||||
|
const withModes = db.prepare(`
|
||||||
|
SELECT COUNT(*) as count FROM nodes
|
||||||
|
WHERE properties_schema LIKE '%resourceLocator%'
|
||||||
|
AND properties_schema LIKE '%modes%'
|
||||||
|
`).get() as { count: number };
|
||||||
|
|
||||||
|
console.log(`Nodes with modes defined: ${withModes.count}`);
|
||||||
|
|
||||||
|
// Query 3: Which nodes have resourceLocator but NO modes?
|
||||||
|
const withoutModes = db.prepare(`
|
||||||
|
SELECT node_type, display_name
|
||||||
|
FROM nodes
|
||||||
|
WHERE properties_schema LIKE '%resourceLocator%'
|
||||||
|
AND properties_schema NOT LIKE '%modes%'
|
||||||
|
LIMIT 10
|
||||||
|
`).all() as Array<{ node_type: string; display_name: string }>;
|
||||||
|
|
||||||
|
console.log(`\nSample nodes WITHOUT modes (showing 10):`);
|
||||||
|
withoutModes.forEach(node => {
|
||||||
|
console.log(` - ${node.display_name} (${node.node_type})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate coverage percentage
|
||||||
|
const coverage = totalResourceLocator.count > 0
|
||||||
|
? (withModes.count / totalResourceLocator.count) * 100
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
console.log(`\nSchema coverage: ${coverage.toFixed(1)}% of resourceLocator nodes have modes defined`);
|
||||||
|
|
||||||
|
// Query 4: Get some examples of nodes WITH modes for verification
|
||||||
|
console.log('\nSample nodes WITH modes (showing 5):');
|
||||||
|
const withModesExamples = db.prepare(`
|
||||||
|
SELECT node_type, display_name
|
||||||
|
FROM nodes
|
||||||
|
WHERE properties_schema LIKE '%resourceLocator%'
|
||||||
|
AND properties_schema LIKE '%modes%'
|
||||||
|
LIMIT 5
|
||||||
|
`).all() as Array<{ node_type: string; display_name: string }>;
|
||||||
|
|
||||||
|
withModesExamples.forEach(node => {
|
||||||
|
console.log(` - ${node.display_name} (${node.node_type})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log('\n=== Summary ===');
|
||||||
|
console.log(`Total nodes in database: ${db.prepare('SELECT COUNT(*) as count FROM nodes').get() as any as { count: number }.count}`);
|
||||||
|
console.log(`Nodes with resourceLocator: ${totalResourceLocator.count}`);
|
||||||
|
console.log(`Nodes with complete mode schemas: ${withModes.count}`);
|
||||||
|
console.log(`Nodes without mode schemas: ${totalResourceLocator.count - withModes.count}`);
|
||||||
|
console.log(`\nImplication: Schema-driven validation will apply to ${withModes.count} nodes.`);
|
||||||
|
console.log(`For the remaining ${totalResourceLocator.count - withModes.count} nodes, validation will be skipped (graceful degradation).`);
|
||||||
|
|
||||||
|
db.close();
|
||||||
@@ -276,17 +276,26 @@ export class ConfigValidator {
|
|||||||
// 1. Object with mode keys: { list: {...}, id: {...}, url: {...}, name: {...} }
|
// 1. Object with mode keys: { list: {...}, id: {...}, url: {...}, name: {...} }
|
||||||
// 2. Array of mode objects: [{name: 'list', ...}, {name: 'id', ...}]
|
// 2. Array of mode objects: [{name: 'list', ...}, {name: 'id', ...}]
|
||||||
const modes = prop.typeOptions.resourceLocator.modes;
|
const modes = prop.typeOptions.resourceLocator.modes;
|
||||||
let allowedModes: string[] = [];
|
|
||||||
|
|
||||||
if (typeof modes === 'object' && !Array.isArray(modes)) {
|
// Validate modes structure before processing to prevent crashes
|
||||||
// Extract keys from modes object
|
if (!modes || typeof modes !== 'object') {
|
||||||
allowedModes = Object.keys(modes);
|
// Invalid schema structure - skip validation to prevent false positives
|
||||||
} else if (Array.isArray(modes)) {
|
continue;
|
||||||
// Extract name property from array of mode objects
|
|
||||||
allowedModes = modes.map(m => typeof m === 'string' ? m : m.name).filter(Boolean);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only validate if we found allowed modes
|
let allowedModes: string[] = [];
|
||||||
|
|
||||||
|
if (Array.isArray(modes)) {
|
||||||
|
// Array format: extract name property from each mode object
|
||||||
|
allowedModes = modes
|
||||||
|
.map(m => (typeof m === 'object' && m !== null) ? m.name : m)
|
||||||
|
.filter(m => typeof m === 'string' && m.length > 0);
|
||||||
|
} else {
|
||||||
|
// Object format: extract keys as mode names
|
||||||
|
allowedModes = Object.keys(modes).filter(k => k.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only validate if we successfully extracted modes
|
||||||
if (allowedModes.length > 0 && !allowedModes.includes(value.mode)) {
|
if (allowedModes.length > 0 && !allowedModes.includes(value.mode)) {
|
||||||
errors.push({
|
errors.push({
|
||||||
type: 'invalid_value',
|
type: 'invalid_value',
|
||||||
|
|||||||
@@ -713,6 +713,117 @@ describe('ConfigValidator - Basic Validation', () => {
|
|||||||
)).toBe(true);
|
)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle modes defined as array format', () => {
|
||||||
|
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
|
||||||
|
const config = {
|
||||||
|
model: {
|
||||||
|
mode: 'custom',
|
||||||
|
value: 'gpt-4o-mini'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const properties = [
|
||||||
|
{
|
||||||
|
name: 'model',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true,
|
||||||
|
typeOptions: {
|
||||||
|
resourceLocator: {
|
||||||
|
modes: [
|
||||||
|
{ name: 'list', displayName: 'List' },
|
||||||
|
{ name: 'id', displayName: 'ID' },
|
||||||
|
{ name: 'custom', displayName: 'Custom' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
||||||
|
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
expect(result.errors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle malformed modes schema gracefully', () => {
|
||||||
|
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
|
||||||
|
const config = {
|
||||||
|
model: {
|
||||||
|
mode: 'any-mode',
|
||||||
|
value: 'gpt-4o-mini'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const properties = [
|
||||||
|
{
|
||||||
|
name: 'model',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true,
|
||||||
|
typeOptions: {
|
||||||
|
resourceLocator: {
|
||||||
|
modes: 'invalid-string' // Malformed schema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
||||||
|
|
||||||
|
// Should NOT crash, should skip validation
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
expect(result.errors.some(e => e.property === 'model.mode')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty modes definition gracefully', () => {
|
||||||
|
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
|
||||||
|
const config = {
|
||||||
|
model: {
|
||||||
|
mode: 'any-mode',
|
||||||
|
value: 'gpt-4o-mini'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const properties = [
|
||||||
|
{
|
||||||
|
name: 'model',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true,
|
||||||
|
typeOptions: {
|
||||||
|
resourceLocator: {
|
||||||
|
modes: {} // Empty object
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
||||||
|
|
||||||
|
// Should skip validation with empty modes
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
expect(result.errors.some(e => e.property === 'model.mode')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip mode validation when typeOptions not provided', () => {
|
||||||
|
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
|
||||||
|
const config = {
|
||||||
|
model: {
|
||||||
|
mode: 'custom-mode',
|
||||||
|
value: 'gpt-4o-mini'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const properties = [
|
||||||
|
{
|
||||||
|
name: 'model',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true
|
||||||
|
// No typeOptions - schema doesn't define modes
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
||||||
|
|
||||||
|
// Should accept any mode when schema doesn't define them
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
expect(result.errors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('should accept resourceLocator with mode "url"', () => {
|
it('should accept resourceLocator with mode "url"', () => {
|
||||||
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
|
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
|
||||||
const config = {
|
const config = {
|
||||||
|
|||||||
Reference in New Issue
Block a user