test: Phase 2 - Create test infrastructure

- Create comprehensive test directory structure
- Implement better-sqlite3 mock for Vitest
- Add node factory using fishery for test data generation
- Create workflow builder with fluent API
- Add infrastructure validation tests
- Update testing checklist to reflect progress

All Phase 2 tasks completed successfully with 7 tests passing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-07-28 13:21:56 +02:00
parent aa3b2a8460
commit 17013d8a25
7 changed files with 893 additions and 7 deletions

View File

@@ -0,0 +1,53 @@
import { vi } from 'vitest';
export class MockDatabase {
private data = new Map<string, any[]>();
private prepared = new Map<string, any>();
constructor() {
this.data.set('nodes', []);
this.data.set('templates', []);
this.data.set('tools_documentation', []);
}
prepare(sql: string) {
const key = this.extractTableName(sql);
return {
all: vi.fn(() => this.data.get(key) || []),
get: vi.fn((id: string) => {
const items = this.data.get(key) || [];
return items.find(item => item.id === id);
}),
run: vi.fn((params: any) => {
const items = this.data.get(key) || [];
items.push(params);
this.data.set(key, items);
return { changes: 1, lastInsertRowid: items.length };
})
};
}
exec(sql: string) {
// Mock schema creation
return true;
}
close() {
// Mock close
return true;
}
// Helper to extract table name from SQL
private extractTableName(sql: string): string {
const match = sql.match(/FROM\s+(\w+)|INTO\s+(\w+)|UPDATE\s+(\w+)/i);
return match ? (match[1] || match[2] || match[3]) : 'nodes';
}
// Test helper to seed data
_seedData(table: string, data: any[]) {
this.data.set(table, data);
}
}
export default vi.fn(() => new MockDatabase());

View File

@@ -0,0 +1,149 @@
import { vi } from 'vitest';
import type { Database } from 'better-sqlite3';
export interface MockDatabase extends Partial<Database> {
prepare: ReturnType<typeof vi.fn>;
exec: ReturnType<typeof vi.fn>;
close: ReturnType<typeof vi.fn>;
transaction: ReturnType<typeof vi.fn>;
pragma: ReturnType<typeof vi.fn>;
backup: ReturnType<typeof vi.fn>;
serialize: ReturnType<typeof vi.fn>;
function: ReturnType<typeof vi.fn>;
aggregate: ReturnType<typeof vi.fn>;
table: ReturnType<typeof vi.fn>;
loadExtension: ReturnType<typeof vi.fn>;
defaultSafeIntegers: ReturnType<typeof vi.fn>;
unsafeMode: ReturnType<typeof vi.fn>;
}
export interface MockStatement {
run: ReturnType<typeof vi.fn>;
get: ReturnType<typeof vi.fn>;
all: ReturnType<typeof vi.fn>;
iterate: ReturnType<typeof vi.fn>;
pluck: ReturnType<typeof vi.fn>;
expand: ReturnType<typeof vi.fn>;
raw: ReturnType<typeof vi.fn>;
columns: ReturnType<typeof vi.fn>;
bind: ReturnType<typeof vi.fn>;
safeIntegers: ReturnType<typeof vi.fn>;
}
export function createMockDatabase(): MockDatabase {
const mockDb: MockDatabase = {
prepare: vi.fn(),
exec: vi.fn(),
close: vi.fn(),
transaction: vi.fn(),
pragma: vi.fn(),
backup: vi.fn(),
serialize: vi.fn(),
function: vi.fn(),
aggregate: vi.fn(),
table: vi.fn(),
loadExtension: vi.fn(),
defaultSafeIntegers: vi.fn(),
unsafeMode: vi.fn(),
memory: false,
readonly: false,
name: ':memory:',
open: true,
inTransaction: false,
};
// Setup default behavior
mockDb.transaction.mockImplementation((fn: Function) => {
return (...args: any[]) => fn(...args);
});
mockDb.pragma.mockReturnValue(undefined);
return mockDb;
}
export function createMockStatement(defaultResults: any = []): MockStatement {
const mockStmt: MockStatement = {
run: vi.fn().mockReturnValue({ changes: 1, lastInsertRowid: 1 }),
get: vi.fn().mockReturnValue(defaultResults[0] || undefined),
all: vi.fn().mockReturnValue(defaultResults),
iterate: vi.fn().mockReturnValue(defaultResults[Symbol.iterator]()),
pluck: vi.fn().mockReturnThis(),
expand: vi.fn().mockReturnThis(),
raw: vi.fn().mockReturnThis(),
columns: vi.fn().mockReturnValue([]),
bind: vi.fn().mockReturnThis(),
safeIntegers: vi.fn().mockReturnThis(),
};
return mockStmt;
}
export function setupDatabaseMock(mockDb: MockDatabase, queryResults: Record<string, any> = {}) {
mockDb.prepare.mockImplementation((query: string) => {
// Match queries to results
for (const [pattern, result] of Object.entries(queryResults)) {
if (query.includes(pattern)) {
return createMockStatement(Array.isArray(result) ? result : [result]);
}
}
// Default mock statement
return createMockStatement();
});
}
// Helper to create a mock node repository
export function createMockNodeRepository() {
return {
getNodeByType: vi.fn(),
searchNodes: vi.fn(),
listNodes: vi.fn(),
getNodeEssentials: vi.fn(),
getNodeDocumentation: vi.fn(),
getNodeInfo: vi.fn(),
searchNodeProperties: vi.fn(),
listAITools: vi.fn(),
getNodeForTask: vi.fn(),
listTasks: vi.fn(),
getDatabaseStatistics: vi.fn(),
close: vi.fn(),
};
}
// Helper to create mock node data
export function createMockNode(overrides: any = {}) {
return {
id: 1,
package_name: 'n8n-nodes-base',
node_type: 'n8n-nodes-base.webhook',
display_name: 'Webhook',
description: 'Starts the workflow when a webhook is called',
version: 2,
defaults: JSON.stringify({ name: 'Webhook' }),
properties: JSON.stringify([]),
credentials: JSON.stringify([]),
inputs: JSON.stringify(['main']),
outputs: JSON.stringify(['main']),
type_version: 2,
is_trigger: 1,
is_regular: 0,
is_webhook: 1,
webhook_path: '/webhook',
full_metadata: JSON.stringify({}),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
...overrides,
};
}
// Helper to create mock query results
export function createMockQueryResults() {
return {
'SELECT * FROM nodes WHERE node_type = ?': createMockNode(),
'SELECT COUNT(*) as count FROM nodes': { count: 525 },
'SELECT * FROM nodes WHERE display_name LIKE ?': [
createMockNode({ node_type: 'n8n-nodes-base.slack', display_name: 'Slack' }),
createMockNode({ node_type: 'n8n-nodes-base.webhook', display_name: 'Webhook' }),
],
};
}

View File

@@ -0,0 +1,140 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { nodeFactory, webhookNodeFactory, slackNodeFactory } from '@tests/fixtures/factories/node.factory';
// Mock better-sqlite3
vi.mock('better-sqlite3');
describe('Test Infrastructure', () => {
describe('Database Mock', () => {
it('should create a mock database instance', async () => {
const Database = (await import('better-sqlite3')).default;
const db = new Database(':memory:');
expect(Database).toHaveBeenCalled();
expect(db).toBeDefined();
expect(db.prepare).toBeDefined();
expect(db.exec).toBeDefined();
expect(db.close).toBeDefined();
});
it('should handle basic CRUD operations', async () => {
const { MockDatabase } = await import('@tests/unit/database/__mocks__/better-sqlite3');
const db = new MockDatabase();
// Test data seeding
db._seedData('nodes', [
{ id: '1', name: 'test-node', type: 'webhook' }
]);
// Test SELECT
const selectStmt = db.prepare('SELECT * FROM nodes');
const allNodes = selectStmt.all();
expect(allNodes).toHaveLength(1);
expect(allNodes[0]).toEqual({ id: '1', name: 'test-node', type: 'webhook' });
// Test INSERT
const insertStmt = db.prepare('INSERT INTO nodes (id, name, type) VALUES (?, ?, ?)');
const result = insertStmt.run({ id: '2', name: 'new-node', type: 'slack' });
expect(result.changes).toBe(1);
// Verify insert worked
const allNodesAfter = selectStmt.all();
expect(allNodesAfter).toHaveLength(2);
});
});
describe('Node Factory', () => {
it('should create a basic node definition', () => {
const node = nodeFactory.build();
expect(node).toMatchObject({
name: expect.any(String),
displayName: expect.any(String),
description: expect.any(String),
version: expect.any(Number),
defaults: {
name: expect.any(String)
},
inputs: ['main'],
outputs: ['main'],
properties: expect.any(Array),
credentials: []
});
});
it('should create a webhook node', () => {
const webhook = webhookNodeFactory.build();
expect(webhook).toMatchObject({
name: 'webhook',
displayName: 'Webhook',
description: 'Starts the workflow when a webhook is called',
group: ['trigger'],
properties: expect.arrayContaining([
expect.objectContaining({
name: 'path',
type: 'string',
required: true
}),
expect.objectContaining({
name: 'method',
type: 'options'
})
])
});
});
it('should create a slack node', () => {
const slack = slackNodeFactory.build();
expect(slack).toMatchObject({
name: 'slack',
displayName: 'Slack',
description: 'Send messages to Slack',
group: ['output'],
credentials: [
{
name: 'slackApi',
required: true
}
],
properties: expect.arrayContaining([
expect.objectContaining({
name: 'resource',
type: 'options'
}),
expect.objectContaining({
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: ['message']
}
}
})
])
});
});
it('should allow overriding factory defaults', () => {
const customNode = nodeFactory.build({
name: 'custom-node',
displayName: 'Custom Node',
version: 2
});
expect(customNode.name).toBe('custom-node');
expect(customNode.displayName).toBe('Custom Node');
expect(customNode.version).toBe(2);
});
it('should create multiple unique nodes', () => {
const nodes = nodeFactory.buildList(5);
expect(nodes).toHaveLength(5);
const names = nodes.map(n => n.name);
const uniqueNames = new Set(names);
expect(uniqueNames.size).toBe(5);
});
});
});