236 lines
7.4 KiB
TypeScript
236 lines
7.4 KiB
TypeScript
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 localDb: smartdb.LocalSmartDb;
|
|
let client: MongoClient;
|
|
let db: Db;
|
|
|
|
function makeTmpDir(): string {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'smartdb-local-test-'));
|
|
}
|
|
|
|
function cleanTmpDir(dir: string): void {
|
|
if (fs.existsSync(dir)) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// LocalSmartDb: Lifecycle
|
|
// ============================================================================
|
|
|
|
tap.test('localsmartdb: should start with just a folder path', async () => {
|
|
tmpDir = makeTmpDir();
|
|
localDb = new smartdb.LocalSmartDb({ folderPath: tmpDir });
|
|
const info = await localDb.start();
|
|
|
|
expect(localDb.running).toBeTrue();
|
|
expect(info.socketPath).toBeTruthy();
|
|
expect(info.connectionUri).toBeTruthy();
|
|
expect(info.connectionUri.startsWith('mongodb://')).toBeTrue();
|
|
});
|
|
|
|
tap.test('localsmartdb: should connect via returned connectionUri', async () => {
|
|
const info = localDb.getConnectionInfo();
|
|
client = new MongoClient(info.connectionUri, {
|
|
directConnection: true,
|
|
serverSelectionTimeoutMS: 5000,
|
|
});
|
|
await client.connect();
|
|
db = client.db('localtest');
|
|
expect(db).toBeTruthy();
|
|
});
|
|
|
|
tap.test('localsmartdb: should reject double start', async () => {
|
|
let threw = false;
|
|
try {
|
|
await localDb.start();
|
|
} catch {
|
|
threw = true;
|
|
}
|
|
expect(threw).toBeTrue();
|
|
});
|
|
|
|
// ============================================================================
|
|
// LocalSmartDb: CRUD via Unix socket
|
|
// ============================================================================
|
|
|
|
tap.test('localsmartdb: insert and find documents', async () => {
|
|
const coll = db.collection('notes');
|
|
await coll.insertMany([
|
|
{ title: 'Note 1', body: 'First note', priority: 1 },
|
|
{ title: 'Note 2', body: 'Second note', priority: 2 },
|
|
{ title: 'Note 3', body: 'Third note', priority: 3 },
|
|
]);
|
|
|
|
const all = await coll.find({}).toArray();
|
|
expect(all.length).toEqual(3);
|
|
|
|
const high = await coll.findOne({ priority: 3 });
|
|
expect(high).toBeTruthy();
|
|
expect(high!.title).toEqual('Note 3');
|
|
});
|
|
|
|
tap.test('localsmartdb: update and verify', async () => {
|
|
const coll = db.collection('notes');
|
|
await coll.updateOne(
|
|
{ title: 'Note 2' },
|
|
{ $set: { body: 'Updated second note', edited: true } }
|
|
);
|
|
|
|
const doc = await coll.findOne({ title: 'Note 2' });
|
|
expect(doc!.body).toEqual('Updated second note');
|
|
expect(doc!.edited).toBeTrue();
|
|
});
|
|
|
|
tap.test('localsmartdb: delete and verify', async () => {
|
|
const coll = db.collection('notes');
|
|
await coll.deleteOne({ title: 'Note 1' });
|
|
|
|
const count = await coll.countDocuments();
|
|
expect(count).toEqual(2);
|
|
|
|
const deleted = await coll.findOne({ title: 'Note 1' });
|
|
expect(deleted).toBeNull();
|
|
});
|
|
|
|
// ============================================================================
|
|
// LocalSmartDb: Persistence across restart
|
|
// ============================================================================
|
|
|
|
tap.test('localsmartdb: stop for restart', async () => {
|
|
await client.close();
|
|
await localDb.stop();
|
|
expect(localDb.running).toBeFalse();
|
|
});
|
|
|
|
tap.test('localsmartdb: restart with same folder', async () => {
|
|
localDb = new smartdb.LocalSmartDb({ folderPath: tmpDir });
|
|
const info = await localDb.start();
|
|
expect(localDb.running).toBeTrue();
|
|
|
|
client = new MongoClient(info.connectionUri, {
|
|
directConnection: true,
|
|
serverSelectionTimeoutMS: 5000,
|
|
});
|
|
await client.connect();
|
|
db = client.db('localtest');
|
|
});
|
|
|
|
tap.test('localsmartdb: data persists after restart', async () => {
|
|
const coll = db.collection('notes');
|
|
|
|
const count = await coll.countDocuments();
|
|
expect(count).toEqual(2); // 3 inserted - 1 deleted
|
|
|
|
const note2 = await coll.findOne({ title: 'Note 2' });
|
|
expect(note2!.body).toEqual('Updated second note');
|
|
expect(note2!.edited).toBeTrue();
|
|
|
|
const note3 = await coll.findOne({ title: 'Note 3' });
|
|
expect(note3!.priority).toEqual(3);
|
|
});
|
|
|
|
// ============================================================================
|
|
// LocalSmartDb: Custom socket path
|
|
// ============================================================================
|
|
|
|
tap.test('localsmartdb: works with custom socket path', async () => {
|
|
await client.close();
|
|
await localDb.stop();
|
|
|
|
const customSocket = path.join(os.tmpdir(), `smartdb-custom-${Date.now()}.sock`);
|
|
const tmpDir2 = makeTmpDir();
|
|
const localDb2 = new smartdb.LocalSmartDb({
|
|
folderPath: tmpDir2,
|
|
socketPath: customSocket,
|
|
});
|
|
|
|
const info = await localDb2.start();
|
|
expect(info.socketPath).toEqual(customSocket);
|
|
|
|
const client2 = new MongoClient(info.connectionUri, {
|
|
directConnection: true,
|
|
serverSelectionTimeoutMS: 5000,
|
|
});
|
|
await client2.connect();
|
|
const testDb = client2.db('customsock');
|
|
await testDb.collection('test').insertOne({ x: 1 });
|
|
const doc = await testDb.collection('test').findOne({ x: 1 });
|
|
expect(doc).toBeTruthy();
|
|
|
|
await client2.close();
|
|
await localDb2.stop();
|
|
cleanTmpDir(tmpDir2);
|
|
|
|
// Reconnect original for remaining tests
|
|
localDb = new smartdb.LocalSmartDb({ folderPath: tmpDir });
|
|
const origInfo = await localDb.start();
|
|
client = new MongoClient(origInfo.connectionUri, {
|
|
directConnection: true,
|
|
serverSelectionTimeoutMS: 5000,
|
|
});
|
|
await client.connect();
|
|
db = client.db('localtest');
|
|
});
|
|
|
|
// ============================================================================
|
|
// LocalSmartDb: getConnectionUri and getServer helpers
|
|
// ============================================================================
|
|
|
|
tap.test('localsmartdb: getConnectionUri returns valid uri', async () => {
|
|
const uri = localDb.getConnectionUri();
|
|
expect(uri.startsWith('mongodb://')).toBeTrue();
|
|
});
|
|
|
|
tap.test('localsmartdb: getServer returns the SmartdbServer', async () => {
|
|
const srv = localDb.getServer();
|
|
expect(srv).toBeTruthy();
|
|
expect(srv.running).toBeTrue();
|
|
});
|
|
|
|
// ============================================================================
|
|
// LocalSmartDb: Data isolation between databases
|
|
// ============================================================================
|
|
|
|
tap.test('localsmartdb: databases are isolated', async () => {
|
|
const dbA = client.db('isoA');
|
|
const dbB = client.db('isoB');
|
|
|
|
await dbA.collection('shared').insertOne({ source: 'A', val: 1 });
|
|
await dbB.collection('shared').insertOne({ source: 'B', val: 2 });
|
|
|
|
const docsA = await dbA.collection('shared').find({}).toArray();
|
|
const docsB = await dbB.collection('shared').find({}).toArray();
|
|
|
|
expect(docsA.length).toEqual(1);
|
|
expect(docsA[0].source).toEqual('A');
|
|
expect(docsB.length).toEqual(1);
|
|
expect(docsB[0].source).toEqual('B');
|
|
|
|
await dbA.dropDatabase();
|
|
await dbB.dropDatabase();
|
|
});
|
|
|
|
// ============================================================================
|
|
// Cleanup
|
|
// ============================================================================
|
|
|
|
tap.test('localsmartdb: cleanup', async () => {
|
|
await client.close();
|
|
await localDb.stop();
|
|
expect(localDb.running).toBeFalse();
|
|
cleanTmpDir(tmpDir);
|
|
});
|
|
|
|
export default tap.start();
|