181 lines
5.6 KiB
TypeScript
181 lines
5.6 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';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test: Unique index enforcement via wire protocol
|
|
// Covers: unique index pre-check, createIndexes persistence, index restoration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
let tmpDir: string;
|
|
let localDb: smartdb.LocalSmartDb;
|
|
let client: MongoClient;
|
|
let db: Db;
|
|
|
|
function makeTmpDir(): string {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'smartdb-unique-test-'));
|
|
}
|
|
|
|
function cleanTmpDir(dir: string): void {
|
|
if (fs.existsSync(dir)) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Setup
|
|
// ============================================================================
|
|
|
|
tap.test('setup: start local db', async () => {
|
|
tmpDir = makeTmpDir();
|
|
localDb = new smartdb.LocalSmartDb({ folderPath: tmpDir });
|
|
const info = await localDb.start();
|
|
client = new MongoClient(info.connectionUri, {
|
|
directConnection: true,
|
|
serverSelectionTimeoutMS: 5000,
|
|
});
|
|
await client.connect();
|
|
db = client.db('uniquetest');
|
|
});
|
|
|
|
// ============================================================================
|
|
// Unique index enforcement on insert
|
|
// ============================================================================
|
|
|
|
tap.test('unique-index: createIndex with unique: true', async () => {
|
|
const coll = db.collection('users');
|
|
await coll.insertOne({ email: 'alice@example.com', name: 'Alice' });
|
|
const indexName = await coll.createIndex({ email: 1 }, { unique: true });
|
|
expect(indexName).toBeTruthy();
|
|
});
|
|
|
|
tap.test('unique-index: reject duplicate on insertOne', async () => {
|
|
const coll = db.collection('users');
|
|
let threw = false;
|
|
try {
|
|
await coll.insertOne({ email: 'alice@example.com', name: 'Alice2' });
|
|
} catch (err: any) {
|
|
threw = true;
|
|
expect(err.code).toEqual(11000);
|
|
}
|
|
expect(threw).toBeTrue();
|
|
|
|
// Verify only 1 document exists
|
|
const count = await coll.countDocuments();
|
|
expect(count).toEqual(1);
|
|
});
|
|
|
|
tap.test('unique-index: allow insert with different unique value', async () => {
|
|
const coll = db.collection('users');
|
|
await coll.insertOne({ email: 'bob@example.com', name: 'Bob' });
|
|
const count = await coll.countDocuments();
|
|
expect(count).toEqual(2);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Unique index enforcement on update
|
|
// ============================================================================
|
|
|
|
tap.test('unique-index: reject duplicate on updateOne that changes unique field', async () => {
|
|
const coll = db.collection('users');
|
|
let threw = false;
|
|
try {
|
|
await coll.updateOne(
|
|
{ email: 'bob@example.com' },
|
|
{ $set: { email: 'alice@example.com' } }
|
|
);
|
|
} catch (err: any) {
|
|
threw = true;
|
|
expect(err.code).toEqual(11000);
|
|
}
|
|
expect(threw).toBeTrue();
|
|
|
|
// Bob's email should be unchanged
|
|
const bob = await coll.findOne({ name: 'Bob' });
|
|
expect(bob!.email).toEqual('bob@example.com');
|
|
});
|
|
|
|
tap.test('unique-index: allow update that keeps same unique value', async () => {
|
|
const coll = db.collection('users');
|
|
await coll.updateOne(
|
|
{ email: 'bob@example.com' },
|
|
{ $set: { name: 'Robert' } }
|
|
);
|
|
const bob = await coll.findOne({ email: 'bob@example.com' });
|
|
expect(bob!.name).toEqual('Robert');
|
|
});
|
|
|
|
// ============================================================================
|
|
// Unique index enforcement on upsert
|
|
// ============================================================================
|
|
|
|
tap.test('unique-index: reject duplicate on upsert insert', async () => {
|
|
const coll = db.collection('users');
|
|
let threw = false;
|
|
try {
|
|
await coll.updateOne(
|
|
{ email: 'new@example.com' },
|
|
{ $set: { email: 'alice@example.com', name: 'Imposter' } },
|
|
{ upsert: true }
|
|
);
|
|
} catch (err: any) {
|
|
threw = true;
|
|
}
|
|
expect(threw).toBeTrue();
|
|
});
|
|
|
|
// ============================================================================
|
|
// Unique index survives restart (persistence + restoration)
|
|
// ============================================================================
|
|
|
|
tap.test('unique-index: stop and restart', async () => {
|
|
await client.close();
|
|
await localDb.stop();
|
|
|
|
localDb = new smartdb.LocalSmartDb({ folderPath: tmpDir });
|
|
const info = await localDb.start();
|
|
client = new MongoClient(info.connectionUri, {
|
|
directConnection: true,
|
|
serverSelectionTimeoutMS: 5000,
|
|
});
|
|
await client.connect();
|
|
db = client.db('uniquetest');
|
|
});
|
|
|
|
tap.test('unique-index: enforcement persists after restart', async () => {
|
|
const coll = db.collection('users');
|
|
|
|
// Data should still be there
|
|
const count = await coll.countDocuments();
|
|
expect(count).toEqual(2);
|
|
|
|
// Unique constraint should still be enforced without calling createIndex again
|
|
let threw = false;
|
|
try {
|
|
await coll.insertOne({ email: 'alice@example.com', name: 'Alice3' });
|
|
} catch (err: any) {
|
|
threw = true;
|
|
expect(err.code).toEqual(11000);
|
|
}
|
|
expect(threw).toBeTrue();
|
|
|
|
// Count unchanged
|
|
const countAfter = await coll.countDocuments();
|
|
expect(countAfter).toEqual(2);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Cleanup
|
|
// ============================================================================
|
|
|
|
tap.test('unique-index: cleanup', async () => {
|
|
await client.close();
|
|
await localDb.stop();
|
|
cleanTmpDir(tmpDir);
|
|
});
|
|
|
|
export default tap.start();
|