test: add Phase 4 database integration tests (partial)
- Add comprehensive test utilities for database testing - Implement connection management tests for in-memory and file databases - Add transaction tests including nested transactions and savepoints - Test database lifecycle, error handling, and performance - Include tests for WAL mode, connection pooling, and constraints Part of Phase 4: Integration Testing
This commit is contained in:
346
tests/integration/database/connection-management.test.ts
Normal file
346
tests/integration/database/connection-management.test.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
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 enforce foreign key constraints', async () => {
|
||||
testDb = new TestDatabase({ mode: 'memory' });
|
||||
const db = await testDb.initialize();
|
||||
|
||||
// Foreign keys should be enabled by default in our schema
|
||||
const fkEnabled = db.prepare('PRAGMA foreign_keys').get() as { foreign_keys: number };
|
||||
expect(fkEnabled.foreign_keys).toBe(1);
|
||||
|
||||
// Test foreign key constraint
|
||||
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)
|
||||
);
|
||||
|
||||
// Try to insert doc for non-existent node (should fail)
|
||||
expect(() => {
|
||||
db.prepare(`
|
||||
INSERT INTO node_docs (node_name, content, examples)
|
||||
VALUES ('non-existent-node', 'content', '[]')
|
||||
`).run();
|
||||
}).toThrow(/FOREIGN KEY constraint failed/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user