- 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
326 lines
10 KiB
TypeScript
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
|
|
});
|
|
});
|
|
}); |