database-utils.test.ts•13.6 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import {
createTestDatabase,
seedTestNodes,
seedTestTemplates,
createTestNode,
createTestTemplate,
resetDatabase,
createDatabaseSnapshot,
restoreDatabaseSnapshot,
loadFixtures,
dbHelpers,
createMockDatabaseAdapter,
withTransaction,
measureDatabaseOperation,
TestDatabase
} from '../../utils/database-utils';
describe('Database Utils', () => {
let testDb: TestDatabase;
afterEach(async () => {
if (testDb) {
await testDb.cleanup();
}
});
describe('createTestDatabase', () => {
it('should create an in-memory database by default', async () => {
testDb = await createTestDatabase();
expect(testDb.adapter).toBeDefined();
expect(testDb.nodeRepository).toBeDefined();
expect(testDb.templateRepository).toBeDefined();
expect(testDb.path).toBe(':memory:');
});
it('should create a file-based database when requested', async () => {
const dbPath = path.join(__dirname, '../../temp/test-file.db');
testDb = await createTestDatabase({ inMemory: false, dbPath });
expect(testDb.path).toBe(dbPath);
expect(fs.existsSync(dbPath)).toBe(true);
});
it('should initialize schema when requested', async () => {
testDb = await createTestDatabase({ initSchema: true });
// Verify tables exist
const tables = testDb.adapter
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
.all() as { name: string }[];
const tableNames = tables.map(t => t.name);
expect(tableNames).toContain('nodes');
expect(tableNames).toContain('templates');
});
it('should skip schema initialization when requested', async () => {
testDb = await createTestDatabase({ initSchema: false });
// Verify tables don't exist (SQLite has internal tables, so check for our specific tables)
const tables = testDb.adapter
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('nodes', 'templates')")
.all() as { name: string }[];
expect(tables.length).toBe(0);
});
});
describe('seedTestNodes', () => {
beforeEach(async () => {
testDb = await createTestDatabase();
});
it('should seed default test nodes', async () => {
const nodes = await seedTestNodes(testDb.nodeRepository);
expect(nodes).toHaveLength(3);
expect(nodes[0].nodeType).toBe('nodes-base.httpRequest');
expect(nodes[1].nodeType).toBe('nodes-base.webhook');
expect(nodes[2].nodeType).toBe('nodes-base.slack');
});
it('should seed custom nodes along with defaults', async () => {
const customNodes = [
{ nodeType: 'nodes-base.custom1', displayName: 'Custom 1' },
{ nodeType: 'nodes-base.custom2', displayName: 'Custom 2' }
];
const nodes = await seedTestNodes(testDb.nodeRepository, customNodes);
expect(nodes).toHaveLength(5); // 3 default + 2 custom
expect(nodes[3].nodeType).toBe('nodes-base.custom1');
expect(nodes[4].nodeType).toBe('nodes-base.custom2');
});
it('should save nodes to database', async () => {
await seedTestNodes(testDb.nodeRepository);
const count = dbHelpers.countRows(testDb.adapter, 'nodes');
expect(count).toBe(3);
const httpNode = testDb.nodeRepository.getNode('nodes-base.httpRequest');
expect(httpNode).toBeDefined();
expect(httpNode.displayName).toBe('HTTP Request');
});
});
describe('seedTestTemplates', () => {
beforeEach(async () => {
testDb = await createTestDatabase();
});
it('should seed default test templates', async () => {
const templates = await seedTestTemplates(testDb.templateRepository);
expect(templates).toHaveLength(2);
expect(templates[0].name).toBe('Simple HTTP Workflow');
expect(templates[1].name).toBe('Webhook to Slack');
});
it('should seed custom templates', async () => {
const customTemplates = [
{ id: 100, name: 'Custom Template' }
];
const templates = await seedTestTemplates(testDb.templateRepository, customTemplates);
expect(templates).toHaveLength(3);
expect(templates[2].id).toBe(100);
expect(templates[2].name).toBe('Custom Template');
});
});
describe('createTestNode', () => {
it('should create a node with defaults', () => {
const node = createTestNode();
expect(node.nodeType).toBe('nodes-base.test');
expect(node.displayName).toBe('Test Node');
expect(node.style).toBe('programmatic');
expect(node.isAITool).toBe(false);
});
it('should override defaults', () => {
const node = createTestNode({
nodeType: 'nodes-base.custom',
displayName: 'Custom Node',
isAITool: true
});
expect(node.nodeType).toBe('nodes-base.custom');
expect(node.displayName).toBe('Custom Node');
expect(node.isAITool).toBe(true);
});
});
describe('resetDatabase', () => {
beforeEach(async () => {
testDb = await createTestDatabase();
});
it('should clear all data and reinitialize schema', async () => {
// Add some data
await seedTestNodes(testDb.nodeRepository);
await seedTestTemplates(testDb.templateRepository);
// Verify data exists
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(3);
expect(dbHelpers.countRows(testDb.adapter, 'templates')).toBe(2);
// Reset database
await resetDatabase(testDb.adapter);
// Verify data is cleared
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(0);
expect(dbHelpers.countRows(testDb.adapter, 'templates')).toBe(0);
// Verify tables still exist
const tables = testDb.adapter
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
.all() as { name: string }[];
const tableNames = tables.map(t => t.name);
expect(tableNames).toContain('nodes');
expect(tableNames).toContain('templates');
});
});
describe('Database Snapshots', () => {
beforeEach(async () => {
testDb = await createTestDatabase();
});
it('should create and restore database snapshot', async () => {
// Seed initial data
await seedTestNodes(testDb.nodeRepository);
await seedTestTemplates(testDb.templateRepository);
// Create snapshot
const snapshot = await createDatabaseSnapshot(testDb.adapter);
expect(snapshot.metadata.nodeCount).toBe(3);
expect(snapshot.metadata.templateCount).toBe(2);
expect(snapshot.nodes).toHaveLength(3);
expect(snapshot.templates).toHaveLength(2);
// Clear database
await resetDatabase(testDb.adapter);
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(0);
// Restore from snapshot
await restoreDatabaseSnapshot(testDb.adapter, snapshot);
// Verify data is restored
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(3);
expect(dbHelpers.countRows(testDb.adapter, 'templates')).toBe(2);
const httpNode = testDb.nodeRepository.getNode('nodes-base.httpRequest');
expect(httpNode).toBeDefined();
expect(httpNode.displayName).toBe('HTTP Request');
});
});
describe('loadFixtures', () => {
beforeEach(async () => {
testDb = await createTestDatabase();
});
it('should load fixtures from JSON file', async () => {
// Create a temporary fixture file
const fixturePath = path.join(__dirname, '../../temp/test-fixtures.json');
const fixtures = {
nodes: [
createTestNode({ nodeType: 'nodes-base.fixture1' }),
createTestNode({ nodeType: 'nodes-base.fixture2' })
],
templates: [
createTestTemplate({ id: 1000, name: 'Fixture Template' })
]
};
// Ensure directory exists
const dir = path.dirname(fixturePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(fixturePath, JSON.stringify(fixtures, null, 2));
// Load fixtures
await loadFixtures(testDb.adapter, fixturePath);
// Verify data was loaded
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(2);
expect(dbHelpers.countRows(testDb.adapter, 'templates')).toBe(1);
expect(dbHelpers.nodeExists(testDb.adapter, 'nodes-base.fixture1')).toBe(true);
expect(dbHelpers.nodeExists(testDb.adapter, 'nodes-base.fixture2')).toBe(true);
// Cleanup
fs.unlinkSync(fixturePath);
});
});
describe('dbHelpers', () => {
beforeEach(async () => {
testDb = await createTestDatabase();
await seedTestNodes(testDb.nodeRepository);
});
it('should count rows correctly', () => {
const count = dbHelpers.countRows(testDb.adapter, 'nodes');
expect(count).toBe(3);
});
it('should check if node exists', () => {
expect(dbHelpers.nodeExists(testDb.adapter, 'nodes-base.httpRequest')).toBe(true);
expect(dbHelpers.nodeExists(testDb.adapter, 'nodes-base.nonexistent')).toBe(false);
});
it('should get all node types', () => {
const nodeTypes = dbHelpers.getAllNodeTypes(testDb.adapter);
expect(nodeTypes).toHaveLength(3);
expect(nodeTypes).toContain('nodes-base.httpRequest');
expect(nodeTypes).toContain('nodes-base.webhook');
expect(nodeTypes).toContain('nodes-base.slack');
});
it('should clear table', () => {
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(3);
dbHelpers.clearTable(testDb.adapter, 'nodes');
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(0);
});
});
describe('createMockDatabaseAdapter', () => {
it('should create a mock adapter with all required methods', () => {
const mockAdapter = createMockDatabaseAdapter();
expect(mockAdapter.prepare).toBeDefined();
expect(mockAdapter.exec).toBeDefined();
expect(mockAdapter.close).toBeDefined();
expect(mockAdapter.pragma).toBeDefined();
expect(mockAdapter.transaction).toBeDefined();
expect(mockAdapter.checkFTS5Support).toBeDefined();
// Test that methods are mocked
expect(vi.isMockFunction(mockAdapter.prepare)).toBe(true);
expect(vi.isMockFunction(mockAdapter.exec)).toBe(true);
});
});
describe('withTransaction', () => {
beforeEach(async () => {
testDb = await createTestDatabase();
});
it('should rollback transaction for testing', async () => {
// Insert a node
await seedTestNodes(testDb.nodeRepository, [
{ nodeType: 'nodes-base.transaction-test' }
]);
const initialCount = dbHelpers.countRows(testDb.adapter, 'nodes');
// Try to insert in a transaction that will rollback
const result = await withTransaction(testDb.adapter, async () => {
testDb.nodeRepository.saveNode(createTestNode({
nodeType: 'nodes-base.should-rollback'
}));
// Verify it was inserted within transaction
const midCount = dbHelpers.countRows(testDb.adapter, 'nodes');
expect(midCount).toBe(initialCount + 1);
return 'test-result';
});
// Transaction should have rolled back
expect(result).toBeNull();
const finalCount = dbHelpers.countRows(testDb.adapter, 'nodes');
expect(finalCount).toBe(initialCount);
});
});
describe('measureDatabaseOperation', () => {
beforeEach(async () => {
testDb = await createTestDatabase();
});
it('should measure operation duration', async () => {
const duration = await measureDatabaseOperation('test operation', async () => {
await seedTestNodes(testDb.nodeRepository);
// Add a small delay to ensure measurable time passes
await new Promise(resolve => setTimeout(resolve, 1));
});
expect(duration).toBeGreaterThanOrEqual(0);
expect(duration).toBeLessThan(1000); // Should be fast
});
});
describe('Integration Tests', () => {
it('should handle complex database operations', async () => {
testDb = await createTestDatabase({ enableFTS5: true });
// Seed initial data
const nodes = await seedTestNodes(testDb.nodeRepository);
const templates = await seedTestTemplates(testDb.templateRepository);
// Create snapshot
const snapshot = await createDatabaseSnapshot(testDb.adapter);
// Add more data
await seedTestNodes(testDb.nodeRepository, [
{ nodeType: 'nodes-base.extra1' },
{ nodeType: 'nodes-base.extra2' }
]);
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(5);
// Restore snapshot
await restoreDatabaseSnapshot(testDb.adapter, snapshot);
// Should be back to original state
expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(3);
// Test FTS5 if supported
if (testDb.adapter.checkFTS5Support()) {
// FTS5 operations would go here
expect(true).toBe(true);
}
});
});
});