Files
n8n-mcp/tests/integration/database/connection-management.test.ts
czlonkowski 253b51f5c6 fix: resolve database integration test issues
- Fix better-sqlite3 import statements to use namespace import
- Update test schemas to match actual database schema
- Align NodeRepository tests with actual API implementation
- Fix FTS5 tests to work with templates instead of nodes
- Update mock data to match ParsedNode interface
- Fix column names to match actual schema (node_type, package_name, etc)
- Add proper ParsedNode creation helper function
- Remove tests for non-existent foreign key constraints
2025-07-29 09:47:44 +02:00

326 lines
10 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import Database from 'better-sqlite3';
import * as fs from 'fs';
import * as path from 'path';
import { TestDatabase, TestDataGenerator } from './test-utils';
describe('Database Connection Management', () => {
let testDb: TestDatabase;
afterEach(async () => {
if (testDb) {
await testDb.cleanup();
}
});
describe('In-Memory Database', () => {
it('should create and connect to in-memory database', async () => {
testDb = new TestDatabase({ mode: 'memory' });
const db = await testDb.initialize();
expect(db).toBeDefined();
expect(db.open).toBe(true);
expect(db.name).toBe(':memory:');
});
it('should execute queries on in-memory database', async () => {
testDb = new TestDatabase({ mode: 'memory' });
const db = await testDb.initialize();
// Test basic query
const result = db.prepare('SELECT 1 as value').get() as { value: number };
expect(result.value).toBe(1);
// Test table exists
const tables = db.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name='nodes'"
).all();
expect(tables.length).toBe(1);
});
it('should handle multiple connections to same in-memory database', async () => {
// Each in-memory database is isolated
const db1 = new TestDatabase({ mode: 'memory' });
const db2 = new TestDatabase({ mode: 'memory' });
const conn1 = await db1.initialize();
const conn2 = await db2.initialize();
// Insert data in first connection
const node = TestDataGenerator.generateNode();
conn1.prepare(`
INSERT INTO nodes (name, type, display_name, package, version, type_version, data)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
node.name,
node.type,
node.displayName,
node.package,
node.version,
node.typeVersion,
JSON.stringify(node)
);
// Verify data is isolated
const count1 = conn1.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
const count2 = conn2.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
expect(count1.count).toBe(1);
expect(count2.count).toBe(0);
await db1.cleanup();
await db2.cleanup();
});
});
describe('File-Based Database', () => {
it('should create and connect to file database', async () => {
testDb = new TestDatabase({ mode: 'file', name: 'test-connection.db' });
const db = await testDb.initialize();
expect(db).toBeDefined();
expect(db.open).toBe(true);
expect(db.name).toContain('test-connection.db');
// Verify file exists
const dbPath = path.join(__dirname, '../../../.test-dbs/test-connection.db');
expect(fs.existsSync(dbPath)).toBe(true);
});
it('should enable WAL mode by default for file databases', async () => {
testDb = new TestDatabase({ mode: 'file', name: 'test-wal.db' });
const db = await testDb.initialize();
const mode = db.prepare('PRAGMA journal_mode').get() as { journal_mode: string };
expect(mode.journal_mode).toBe('wal');
// Verify WAL files are created
const dbPath = path.join(__dirname, '../../../.test-dbs/test-wal.db');
expect(fs.existsSync(`${dbPath}-wal`)).toBe(true);
expect(fs.existsSync(`${dbPath}-shm`)).toBe(true);
});
it('should allow disabling WAL mode', async () => {
testDb = new TestDatabase({
mode: 'file',
name: 'test-no-wal.db',
enableWAL: false
});
const db = await testDb.initialize();
const mode = db.prepare('PRAGMA journal_mode').get() as { journal_mode: string };
expect(mode.journal_mode).not.toBe('wal');
});
it('should handle connection pooling simulation', async () => {
const dbPath = path.join(__dirname, '../../../.test-dbs/test-pool.db');
// Create initial database
testDb = new TestDatabase({ mode: 'file', name: 'test-pool.db' });
await testDb.initialize();
await testDb.cleanup();
// Simulate multiple connections
const connections: Database.Database[] = [];
const connectionCount = 5;
try {
for (let i = 0; i < connectionCount; i++) {
const conn = new Database(dbPath, {
readonly: false,
fileMustExist: true
});
connections.push(conn);
}
// All connections should be open
expect(connections.every(conn => conn.open)).toBe(true);
// Test concurrent reads
const promises = connections.map((conn, index) => {
return new Promise((resolve) => {
const result = conn.prepare('SELECT ? as id').get(index);
resolve(result);
});
});
const results = await Promise.all(promises);
expect(results).toHaveLength(connectionCount);
} finally {
// Cleanup connections
connections.forEach(conn => conn.close());
if (fs.existsSync(dbPath)) {
fs.unlinkSync(dbPath);
fs.unlinkSync(`${dbPath}-wal`);
fs.unlinkSync(`${dbPath}-shm`);
}
}
});
});
describe('Connection Error Handling', () => {
it('should handle invalid file path gracefully', async () => {
const invalidPath = '/invalid/path/that/does/not/exist/test.db';
expect(() => {
new Database(invalidPath);
}).toThrow();
});
it('should handle database file corruption', async () => {
const corruptPath = path.join(__dirname, '../../../.test-dbs/corrupt.db');
// Create directory if it doesn't exist
const dir = path.dirname(corruptPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// Create a corrupt database file
fs.writeFileSync(corruptPath, 'This is not a valid SQLite database');
try {
expect(() => {
new Database(corruptPath);
}).toThrow();
} finally {
if (fs.existsSync(corruptPath)) {
fs.unlinkSync(corruptPath);
}
}
});
it('should handle readonly database access', async () => {
// Create a database first
testDb = new TestDatabase({ mode: 'file', name: 'test-readonly.db' });
const db = await testDb.initialize();
// Insert test data
const node = TestDataGenerator.generateNode();
db.prepare(`
INSERT INTO nodes (name, type, display_name, package, version, type_version, data)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
node.name,
node.type,
node.displayName,
node.package,
node.version,
node.typeVersion,
JSON.stringify(node)
);
const dbPath = path.join(__dirname, '../../../.test-dbs/test-readonly.db');
// Open as readonly
const readonlyDb = new Database(dbPath, { readonly: true });
try {
// Reading should work
const count = readonlyDb.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
expect(count.count).toBe(1);
// Writing should fail
expect(() => {
readonlyDb.prepare('DELETE FROM nodes').run();
}).toThrow(/readonly/);
} finally {
readonlyDb.close();
}
});
});
describe('Connection Lifecycle', () => {
it('should properly close database connections', async () => {
testDb = new TestDatabase({ mode: 'file', name: 'test-lifecycle.db' });
const db = await testDb.initialize();
expect(db.open).toBe(true);
await testDb.cleanup();
expect(db.open).toBe(false);
});
it('should handle multiple open/close cycles', async () => {
const dbPath = path.join(__dirname, '../../../.test-dbs/test-cycles.db');
for (let i = 0; i < 3; i++) {
const db = new TestDatabase({ mode: 'file', name: 'test-cycles.db' });
const conn = await db.initialize();
// Perform operation
const result = conn.prepare('SELECT ? as cycle').get(i) as { cycle: number };
expect(result.cycle).toBe(i);
await db.cleanup();
}
// Ensure file is cleaned up
expect(fs.existsSync(dbPath)).toBe(false);
});
it('should handle connection timeout simulation', async () => {
testDb = new TestDatabase({ mode: 'file', name: 'test-timeout.db' });
const db = await testDb.initialize();
// Set a busy timeout
db.exec('PRAGMA busy_timeout = 100'); // 100ms timeout
// Start a transaction to lock the database
db.exec('BEGIN EXCLUSIVE');
// Try to access from another connection (should timeout)
const dbPath = path.join(__dirname, '../../../.test-dbs/test-timeout.db');
const conn2 = new Database(dbPath);
conn2.exec('PRAGMA busy_timeout = 100');
try {
expect(() => {
conn2.exec('BEGIN EXCLUSIVE');
}).toThrow(/database is locked/);
} finally {
db.exec('ROLLBACK');
conn2.close();
}
});
});
describe('Database Configuration', () => {
it('should apply optimal pragmas for performance', async () => {
testDb = new TestDatabase({ mode: 'file', name: 'test-pragmas.db' });
const db = await testDb.initialize();
// Apply performance pragmas
db.exec('PRAGMA synchronous = NORMAL');
db.exec('PRAGMA cache_size = -64000'); // 64MB cache
db.exec('PRAGMA temp_store = MEMORY');
db.exec('PRAGMA mmap_size = 268435456'); // 256MB mmap
// Verify pragmas
const sync = db.prepare('PRAGMA synchronous').get() as { synchronous: number };
const cache = db.prepare('PRAGMA cache_size').get() as { cache_size: number };
const temp = db.prepare('PRAGMA temp_store').get() as { temp_store: number };
const mmap = db.prepare('PRAGMA mmap_size').get() as { mmap_size: number };
expect(sync.synchronous).toBe(1); // NORMAL = 1
expect(cache.cache_size).toBe(-64000);
expect(temp.temp_store).toBe(2); // MEMORY = 2
expect(mmap.mmap_size).toBeGreaterThan(0);
});
it('should have foreign key support enabled', async () => {
testDb = new TestDatabase({ mode: 'memory' });
const db = await testDb.initialize();
// Foreign keys should be enabled by default
const fkEnabled = db.prepare('PRAGMA foreign_keys').get() as { foreign_keys: number };
expect(fkEnabled.foreign_keys).toBe(1);
// Note: The current schema doesn't define foreign key constraints,
// but the setting is enabled for future use
});
});
});