feat(test): add integration coverage for file storage, compaction, migration, and LocalSmartDb workflows
This commit is contained in:
394
test/test.file-storage.ts
Normal file
394
test/test.file-storage.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartdb from '../ts/index.js';
|
||||
import { MongoClient, Db } from 'mongodb';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let tmpDir: string;
|
||||
let server: smartdb.SmartdbServer;
|
||||
let client: MongoClient;
|
||||
let db: Db;
|
||||
|
||||
function makeTmpDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'smartdb-test-'));
|
||||
}
|
||||
|
||||
function cleanTmpDir(dir: string): void {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// File Storage: Startup
|
||||
// ============================================================================
|
||||
|
||||
tap.test('file-storage: should start server with file storage', async () => {
|
||||
tmpDir = makeTmpDir();
|
||||
server = new smartdb.SmartdbServer({
|
||||
port: 27118,
|
||||
storage: 'file',
|
||||
storagePath: tmpDir,
|
||||
});
|
||||
await server.start();
|
||||
expect(server.running).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('file-storage: should connect MongoClient', async () => {
|
||||
client = new MongoClient('mongodb://127.0.0.1:27118', {
|
||||
directConnection: true,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
});
|
||||
await client.connect();
|
||||
db = client.db('filetest');
|
||||
expect(db).toBeTruthy();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// File Storage: Data files are created on disk
|
||||
// ============================================================================
|
||||
|
||||
tap.test('file-storage: inserting creates data files on disk', async () => {
|
||||
const coll = db.collection('diskcheck');
|
||||
await coll.insertOne({ name: 'disk-test', value: 42 });
|
||||
|
||||
// The storage directory should now contain a database directory
|
||||
const dbDir = path.join(tmpDir, 'filetest');
|
||||
expect(fs.existsSync(dbDir)).toBeTrue();
|
||||
|
||||
// Collection directory with data.rdb should exist
|
||||
const collDir = path.join(dbDir, 'diskcheck');
|
||||
expect(fs.existsSync(collDir)).toBeTrue();
|
||||
|
||||
const dataFile = path.join(collDir, 'data.rdb');
|
||||
expect(fs.existsSync(dataFile)).toBeTrue();
|
||||
|
||||
// data.rdb should have the SMARTDB magic header
|
||||
const header = Buffer.alloc(8);
|
||||
const fd = fs.openSync(dataFile, 'r');
|
||||
fs.readSync(fd, header, 0, 8, 0);
|
||||
fs.closeSync(fd);
|
||||
expect(header.toString('ascii')).toEqual('SMARTDB\0');
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// File Storage: Full CRUD cycle
|
||||
// ============================================================================
|
||||
|
||||
tap.test('file-storage: insertOne returns valid id', async () => {
|
||||
const coll = db.collection('crud');
|
||||
const result = await coll.insertOne({ name: 'Alice', age: 30 });
|
||||
expect(result.acknowledged).toBeTrue();
|
||||
expect(result.insertedId).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('file-storage: insertMany returns all ids', async () => {
|
||||
const coll = db.collection('crud');
|
||||
const result = await coll.insertMany([
|
||||
{ name: 'Bob', age: 25 },
|
||||
{ name: 'Charlie', age: 35 },
|
||||
{ name: 'Diana', age: 28 },
|
||||
{ name: 'Eve', age: 32 },
|
||||
]);
|
||||
expect(result.insertedCount).toEqual(4);
|
||||
});
|
||||
|
||||
tap.test('file-storage: findOne retrieves correct document', async () => {
|
||||
const coll = db.collection('crud');
|
||||
const doc = await coll.findOne({ name: 'Alice' });
|
||||
expect(doc).toBeTruthy();
|
||||
expect(doc!.name).toEqual('Alice');
|
||||
expect(doc!.age).toEqual(30);
|
||||
});
|
||||
|
||||
tap.test('file-storage: find with filter returns correct subset', async () => {
|
||||
const coll = db.collection('crud');
|
||||
const docs = await coll.find({ age: { $gte: 30 } }).toArray();
|
||||
expect(docs.length).toEqual(3); // Alice(30), Charlie(35), Eve(32)
|
||||
expect(docs.every(d => d.age >= 30)).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('file-storage: updateOne modifies document', async () => {
|
||||
const coll = db.collection('crud');
|
||||
const result = await coll.updateOne(
|
||||
{ name: 'Alice' },
|
||||
{ $set: { age: 31, updated: true } }
|
||||
);
|
||||
expect(result.modifiedCount).toEqual(1);
|
||||
|
||||
const doc = await coll.findOne({ name: 'Alice' });
|
||||
expect(doc!.age).toEqual(31);
|
||||
expect(doc!.updated).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('file-storage: deleteOne removes document', async () => {
|
||||
const coll = db.collection('crud');
|
||||
const result = await coll.deleteOne({ name: 'Eve' });
|
||||
expect(result.deletedCount).toEqual(1);
|
||||
|
||||
const doc = await coll.findOne({ name: 'Eve' });
|
||||
expect(doc).toBeNull();
|
||||
});
|
||||
|
||||
tap.test('file-storage: count reflects current state', async () => {
|
||||
const coll = db.collection('crud');
|
||||
const count = await coll.countDocuments();
|
||||
expect(count).toEqual(4); // 5 inserted - 1 deleted = 4
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// File Storage: Persistence across server restart
|
||||
// ============================================================================
|
||||
|
||||
tap.test('file-storage: stop server for restart test', async () => {
|
||||
await client.close();
|
||||
await server.stop();
|
||||
expect(server.running).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('file-storage: restart server with same data path', async () => {
|
||||
server = new smartdb.SmartdbServer({
|
||||
port: 27118,
|
||||
storage: 'file',
|
||||
storagePath: tmpDir,
|
||||
});
|
||||
await server.start();
|
||||
expect(server.running).toBeTrue();
|
||||
|
||||
client = new MongoClient('mongodb://127.0.0.1:27118', {
|
||||
directConnection: true,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
});
|
||||
await client.connect();
|
||||
db = client.db('filetest');
|
||||
});
|
||||
|
||||
tap.test('file-storage: data persists after restart', async () => {
|
||||
const coll = db.collection('crud');
|
||||
|
||||
// Alice should still be there with updated age
|
||||
const alice = await coll.findOne({ name: 'Alice' });
|
||||
expect(alice).toBeTruthy();
|
||||
expect(alice!.age).toEqual(31);
|
||||
expect(alice!.updated).toBeTrue();
|
||||
|
||||
// Bob, Charlie, Diana should be there
|
||||
const bob = await coll.findOne({ name: 'Bob' });
|
||||
expect(bob).toBeTruthy();
|
||||
expect(bob!.age).toEqual(25);
|
||||
|
||||
const charlie = await coll.findOne({ name: 'Charlie' });
|
||||
expect(charlie).toBeTruthy();
|
||||
|
||||
const diana = await coll.findOne({ name: 'Diana' });
|
||||
expect(diana).toBeTruthy();
|
||||
|
||||
// Eve should still be deleted
|
||||
const eve = await coll.findOne({ name: 'Eve' });
|
||||
expect(eve).toBeNull();
|
||||
});
|
||||
|
||||
tap.test('file-storage: count is correct after restart', async () => {
|
||||
const coll = db.collection('crud');
|
||||
const count = await coll.countDocuments();
|
||||
expect(count).toEqual(4);
|
||||
});
|
||||
|
||||
tap.test('file-storage: can write new data after restart', async () => {
|
||||
const coll = db.collection('crud');
|
||||
const result = await coll.insertOne({ name: 'Frank', age: 45 });
|
||||
expect(result.acknowledged).toBeTrue();
|
||||
|
||||
const doc = await coll.findOne({ name: 'Frank' });
|
||||
expect(doc).toBeTruthy();
|
||||
expect(doc!.age).toEqual(45);
|
||||
|
||||
const count = await coll.countDocuments();
|
||||
expect(count).toEqual(5);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// File Storage: Multiple collections in same database
|
||||
// ============================================================================
|
||||
|
||||
tap.test('file-storage: multiple collections are independent', async () => {
|
||||
const products = db.collection('products');
|
||||
const orders = db.collection('orders');
|
||||
|
||||
await products.insertMany([
|
||||
{ sku: 'A001', name: 'Widget', price: 9.99 },
|
||||
{ sku: 'A002', name: 'Gadget', price: 19.99 },
|
||||
]);
|
||||
|
||||
await orders.insertMany([
|
||||
{ orderId: 1, sku: 'A001', qty: 3 },
|
||||
{ orderId: 2, sku: 'A002', qty: 1 },
|
||||
{ orderId: 3, sku: 'A001', qty: 2 },
|
||||
]);
|
||||
|
||||
const productCount = await products.countDocuments();
|
||||
const orderCount = await orders.countDocuments();
|
||||
expect(productCount).toEqual(2);
|
||||
expect(orderCount).toEqual(3);
|
||||
|
||||
// Deleting from one collection doesn't affect the other
|
||||
await products.deleteOne({ sku: 'A001' });
|
||||
expect(await products.countDocuments()).toEqual(1);
|
||||
expect(await orders.countDocuments()).toEqual(3);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// File Storage: Multiple databases
|
||||
// ============================================================================
|
||||
|
||||
tap.test('file-storage: multiple databases are independent', async () => {
|
||||
const db2 = client.db('filetest2');
|
||||
const coll2 = db2.collection('items');
|
||||
|
||||
await coll2.insertOne({ name: 'cross-db-test', source: 'db2' });
|
||||
|
||||
// db2 has 1 doc
|
||||
const count2 = await coll2.countDocuments();
|
||||
expect(count2).toEqual(1);
|
||||
|
||||
// original db is unaffected
|
||||
const crudCount = await db.collection('crud').countDocuments();
|
||||
expect(crudCount).toEqual(5);
|
||||
|
||||
await db2.dropDatabase();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// File Storage: Large batch insert and retrieval
|
||||
// ============================================================================
|
||||
|
||||
tap.test('file-storage: bulk insert 1000 documents', async () => {
|
||||
const coll = db.collection('bulk');
|
||||
const docs = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
docs.push({ index: i, data: `value-${i}`, timestamp: Date.now() });
|
||||
}
|
||||
const result = await coll.insertMany(docs);
|
||||
expect(result.insertedCount).toEqual(1000);
|
||||
});
|
||||
|
||||
tap.test('file-storage: find all 1000 documents', async () => {
|
||||
const coll = db.collection('bulk');
|
||||
const docs = await coll.find({}).toArray();
|
||||
expect(docs.length).toEqual(1000);
|
||||
});
|
||||
|
||||
tap.test('file-storage: range query on 1000 documents', async () => {
|
||||
const coll = db.collection('bulk');
|
||||
const docs = await coll.find({ index: { $gte: 500, $lt: 600 } }).toArray();
|
||||
expect(docs.length).toEqual(100);
|
||||
expect(docs.every(d => d.index >= 500 && d.index < 600)).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('file-storage: sorted retrieval with limit', async () => {
|
||||
const coll = db.collection('bulk');
|
||||
const docs = await coll.find({}).sort({ index: -1 }).limit(10).toArray();
|
||||
expect(docs.length).toEqual(10);
|
||||
expect(docs[0].index).toEqual(999);
|
||||
expect(docs[9].index).toEqual(990);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// File Storage: Update many and verify persistence
|
||||
// ============================================================================
|
||||
|
||||
tap.test('file-storage: updateMany on bulk collection', async () => {
|
||||
const coll = db.collection('bulk');
|
||||
const result = await coll.updateMany(
|
||||
{ index: { $lt: 100 } },
|
||||
{ $set: { batch: 'first-hundred' } }
|
||||
);
|
||||
expect(result.modifiedCount).toEqual(100);
|
||||
|
||||
const updated = await coll.find({ batch: 'first-hundred' }).toArray();
|
||||
expect(updated.length).toEqual(100);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// File Storage: Delete many and verify
|
||||
// ============================================================================
|
||||
|
||||
tap.test('file-storage: deleteMany removes correct documents', async () => {
|
||||
const coll = db.collection('bulk');
|
||||
const result = await coll.deleteMany({ index: { $gte: 900 } });
|
||||
expect(result.deletedCount).toEqual(100);
|
||||
|
||||
const remaining = await coll.countDocuments();
|
||||
expect(remaining).toEqual(900);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// File Storage: Persistence of bulk data across restart
|
||||
// ============================================================================
|
||||
|
||||
tap.test('file-storage: stop server for bulk restart test', async () => {
|
||||
await client.close();
|
||||
await server.stop();
|
||||
expect(server.running).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('file-storage: restart and verify bulk data', async () => {
|
||||
server = new smartdb.SmartdbServer({
|
||||
port: 27118,
|
||||
storage: 'file',
|
||||
storagePath: tmpDir,
|
||||
});
|
||||
await server.start();
|
||||
|
||||
client = new MongoClient('mongodb://127.0.0.1:27118', {
|
||||
directConnection: true,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
});
|
||||
await client.connect();
|
||||
db = client.db('filetest');
|
||||
|
||||
const coll = db.collection('bulk');
|
||||
const count = await coll.countDocuments();
|
||||
expect(count).toEqual(900);
|
||||
|
||||
// Verify the updateMany persisted
|
||||
const firstHundred = await coll.find({ batch: 'first-hundred' }).toArray();
|
||||
expect(firstHundred.length).toEqual(100);
|
||||
|
||||
// Verify deleted docs are gone
|
||||
const over900 = await coll.find({ index: { $gte: 900 } }).toArray();
|
||||
expect(over900.length).toEqual(0);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// File Storage: Index persistence
|
||||
// ============================================================================
|
||||
|
||||
tap.test('file-storage: default indexes.json exists on disk', async () => {
|
||||
// The indexes.json is created when the collection is first created,
|
||||
// containing the default _id_ index spec.
|
||||
const indexFile = path.join(tmpDir, 'filetest', 'crud', 'indexes.json');
|
||||
expect(fs.existsSync(indexFile)).toBeTrue();
|
||||
|
||||
const indexData = JSON.parse(fs.readFileSync(indexFile, 'utf-8'));
|
||||
const names = indexData.map((i: any) => i.name);
|
||||
expect(names).toContain('_id_');
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Cleanup
|
||||
// ============================================================================
|
||||
|
||||
tap.test('file-storage: cleanup', async () => {
|
||||
await client.close();
|
||||
await server.stop();
|
||||
expect(server.running).toBeFalse();
|
||||
cleanTmpDir(tmpDir);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user