Files
smartdb/test/test.file-storage.ts

395 lines
13 KiB
TypeScript
Raw Permalink Normal View History

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();