161 lines
5.4 KiB
TypeScript
161 lines
5.4 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import * as smartdb from '../ts/index.js';
|
|
import { MongoClient } from 'mongodb';
|
|
import * as net from 'net';
|
|
|
|
let server: smartdb.SmartdbServer;
|
|
let client: MongoClient;
|
|
let port: number;
|
|
|
|
async function getFreePort(): Promise<number> {
|
|
return await new Promise((resolve, reject) => {
|
|
const probe = net.createServer();
|
|
probe.once('error', reject);
|
|
probe.listen(0, '127.0.0.1', () => {
|
|
const address = probe.address();
|
|
if (!address || typeof address === 'string') {
|
|
probe.close(() => reject(new Error('Failed to allocate TCP port')));
|
|
return;
|
|
}
|
|
probe.close(() => resolve(address.port));
|
|
});
|
|
});
|
|
}
|
|
|
|
tap.test('transactions: should start server and connect', async () => {
|
|
port = await getFreePort();
|
|
server = new smartdb.SmartdbServer({ port });
|
|
await server.start();
|
|
|
|
client = new MongoClient(`mongodb://127.0.0.1:${port}`, {
|
|
directConnection: true,
|
|
serverSelectionTimeoutMS: 5000,
|
|
});
|
|
await client.connect();
|
|
expect(server.running).toBeTrue();
|
|
});
|
|
|
|
tap.test('transactions: should still support explicit sessions', async () => {
|
|
const result = await client.db('admin').command({ startSession: 1 });
|
|
expect(result.ok).toEqual(1);
|
|
expect(result.id).toBeTruthy();
|
|
|
|
const end = await client.db('admin').command({ endSessions: [result.id] });
|
|
expect(end.ok).toEqual(1);
|
|
});
|
|
|
|
tap.test('transactions: should reject transaction-scoped writes without txnNumber before mutation', async () => {
|
|
const db = client.db('txntest');
|
|
const coll = db.collection('docs');
|
|
await coll.insertOne({ key: 'outside', value: 1 });
|
|
|
|
let threw = false;
|
|
try {
|
|
await db.command({
|
|
insert: 'docs',
|
|
documents: [{ key: 'inside-raw', value: 2 }],
|
|
startTransaction: true,
|
|
autocommit: false,
|
|
});
|
|
} catch (err: any) {
|
|
threw = true;
|
|
expect(err.code).toEqual(14);
|
|
expect(err.codeName).toEqual('TypeMismatch');
|
|
}
|
|
expect(threw).toBeTrue();
|
|
|
|
expect(await coll.countDocuments({ key: 'inside-raw' })).toEqual(0);
|
|
expect(await coll.countDocuments({ key: 'outside' })).toEqual(1);
|
|
});
|
|
|
|
tap.test('transactions: official driver transaction should commit buffered writes', async () => {
|
|
const coll = client.db('txntest').collection('driverdocs');
|
|
await coll.insertOne({ key: 'outside-driver', value: 0 });
|
|
const session = client.startSession();
|
|
|
|
try {
|
|
session.startTransaction();
|
|
await coll.insertOne({ key: 'inside-driver', value: 1 }, { session });
|
|
const inTxn = await coll.findOne({ key: 'inside-driver' }, { session });
|
|
expect(inTxn).toBeTruthy();
|
|
expect(await coll.countDocuments({ key: 'inside-driver' })).toEqual(0);
|
|
await session.commitTransaction();
|
|
} finally {
|
|
await session.endSession();
|
|
}
|
|
|
|
expect(await coll.countDocuments({ key: 'inside-driver' })).toEqual(1);
|
|
expect(await coll.countDocuments({ key: 'outside-driver' })).toEqual(1);
|
|
});
|
|
|
|
tap.test('transactions: abort should discard buffered writes', async () => {
|
|
const coll = client.db('txntest').collection('abortdocs');
|
|
const session = client.startSession();
|
|
|
|
try {
|
|
session.startTransaction();
|
|
await coll.insertOne({ key: 'abort-me', value: 1 }, { session });
|
|
expect(await coll.findOne({ key: 'abort-me' }, { session })).toBeTruthy();
|
|
await session.abortTransaction();
|
|
} finally {
|
|
await session.endSession();
|
|
}
|
|
|
|
expect(await coll.findOne({ key: 'abort-me' })).toBeNull();
|
|
});
|
|
|
|
tap.test('transactions: update and delete should commit atomically', async () => {
|
|
const coll = client.db('txntest').collection('mutations');
|
|
await coll.insertMany([
|
|
{ key: 'update-me', value: 1 },
|
|
{ key: 'delete-me', value: 2 },
|
|
]);
|
|
const session = client.startSession();
|
|
|
|
try {
|
|
session.startTransaction();
|
|
await coll.updateOne({ key: 'update-me' }, { $set: { value: 10 } }, { session });
|
|
await coll.deleteOne({ key: 'delete-me' }, { session });
|
|
expect((await coll.findOne({ key: 'update-me' }, { session }))!.value).toEqual(10);
|
|
expect(await coll.findOne({ key: 'delete-me' }, { session })).toBeNull();
|
|
expect((await coll.findOne({ key: 'update-me' }))!.value).toEqual(1);
|
|
expect(await coll.findOne({ key: 'delete-me' })).toBeTruthy();
|
|
await session.commitTransaction();
|
|
} finally {
|
|
await session.endSession();
|
|
}
|
|
|
|
expect((await coll.findOne({ key: 'update-me' }))!.value).toEqual(10);
|
|
expect(await coll.findOne({ key: 'delete-me' })).toBeNull();
|
|
});
|
|
|
|
tap.test('transactions: commit and abort without active transaction should be explicit errors', async () => {
|
|
for (const command of [{ commitTransaction: 1 }, { abortTransaction: 1 }]) {
|
|
let threw = false;
|
|
try {
|
|
await client.db('admin').command(command);
|
|
} catch (err: any) {
|
|
threw = true;
|
|
expect(err.code).toEqual(251);
|
|
expect(err.codeName).toEqual('NoSuchTransaction');
|
|
}
|
|
expect(threw).toBeTrue();
|
|
}
|
|
});
|
|
|
|
tap.test('transactions: serverStatus should expose transaction and oplog metrics', async () => {
|
|
const status = await client.db('admin').command({ serverStatus: 1 });
|
|
expect(status.ok).toEqual(1);
|
|
expect(status.transactions.currentActive).toEqual(0);
|
|
expect(status.logicalSessionRecordCache.activeSessionsCount).toBeGreaterThanOrEqual(0);
|
|
expect(status.oplog.totalEntries).toBeGreaterThan(0);
|
|
});
|
|
|
|
tap.test('transactions: cleanup', async () => {
|
|
await client.close();
|
|
await server.stop();
|
|
expect(server.running).toBeFalse();
|
|
});
|
|
|
|
export default tap.start();
|