// E2E part 4: TTL indexes through the official Node driver. // createIndex({expireAfterSeconds}), the option round-tripping through // listIndexes, background expiry of past-dated documents, and the specs the // server must reject with CannotCreateIndex (67). // // The server must run with a short sweep interval, e.g. // zig-out/bin/multiforadb --port 27020 --db /tmp/mfdb-e2e.log --ttl-sweep-secs 1 const { MongoClient } = require('mongodb'); const URL = 'mongodb://127.0.0.1:27020'; const results = []; function check(name, cond, detail = '') { results.push({ name, ok: !!cond, detail: String(detail) }); if (!cond) console.error(` ✗ ${name} ${detail}`); } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); /// Poll until `fn()` is true or the deadline passes; expiry is a background /// sweep, so the exact moment a document disappears is not fixed. async function waitFor(fn, timeoutMs = 15000, stepMs = 250) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (await fn()) return true; await sleep(stepMs); } return false; } async function expectCode(fn, code, name) { let err = null; try { await fn(); } catch (e) { err = e; } check(name, err && err.code === code, err ? `code ${err.code}: ${err.message}` : 'no error'); } async function main() { const client = new MongoClient(URL, { serverSelectionTimeoutMS: 5000 }); await client.connect(); const db = client.db('e2e4'); const coll = db.collection('sessions'); await coll.drop().catch(() => {}); // --- createIndex + listIndexes round-trip --- await coll.createIndex({ expireAt: 1 }, { expireAfterSeconds: 1 }); const idxs = await coll.indexes(); const ttlIdx = idxs.find((i) => i.name === 'expireAt_1'); check('TTL index listed', !!ttlIdx, JSON.stringify(idxs)); check('expireAfterSeconds reported', ttlIdx && Number(ttlIdx.expireAfterSeconds) === 1, JSON.stringify(ttlIdx)); check('_id_ has no expireAfterSeconds', idxs.find((i) => i.name === '_id_').expireAfterSeconds === undefined); // Idempotent re-create of the same spec. await coll.createIndex({ expireAt: 1 }, { expireAfterSeconds: 1 }); check('idempotent re-create', (await coll.indexes()).filter((i) => i.name === 'expireAt_1').length === 1); // --- expiry --- const now = Date.now(); await coll.insertMany([ { _id: 'past', expireAt: new Date(now - 60_000) }, { _id: 'future', expireAt: new Date(now + 3_600_000) }, { _id: 'string', expireAt: 'tomorrow' }, { _id: 'missing' }, // An array of dates expires on its earliest member. { _id: 'array', expireAt: [new Date(now - 60_000), new Date(now + 3_600_000)] }, ]); check('all five inserted', (await coll.countDocuments({})) === 5); const gone = await waitFor(async () => (await coll.countDocuments({ _id: 'past' })) === 0); check('past-dated doc expired', gone); check('array doc expired on earliest date', (await coll.countDocuments({ _id: 'array' })) === 0); const survivors = (await coll.find({}).toArray()).map((d) => d._id).sort(); check('future/string/missing survive', JSON.stringify(survivors) === '["future","missing","string"]', JSON.stringify(survivors)); // Expiry is a real delete: it holds after the sweeper has run again. await sleep(1500); check('expired docs stay deleted', (await coll.countDocuments({})) === 3); // --- rejected specs --- const bad = db.collection('bad'); await expectCode( () => bad.createIndex({ a: 1, b: 1 }, { expireAfterSeconds: 60 }), 67, 'compound TTL rejected with 67', ); await expectCode( () => bad.createIndex({ a: 1 }, { expireAfterSeconds: -1 }), 67, 'negative expireAfterSeconds rejected with 67', ); await expectCode( () => bad.createIndex({ a: 1 }, { expireAfterSeconds: 'soon' }), 67, 'non-numeric expireAfterSeconds rejected with 67', ); await expectCode( () => bad.createIndex({ a: 1 }, { expireAfterSeconds: 2147483648 }), 67, 'expireAfterSeconds past 2147483647 rejected with 67', ); await expectCode( () => bad.createIndex({ _id: 1 }, { expireAfterSeconds: 60 }), 197, 'TTL on _id rejected with 197', ); // Same name and key with a different expiry: IndexOptionsConflict (85), // exactly as MongoDB does (the change belongs to collMod). await expectCode( () => coll.createIndex({ expireAt: 1 }, { expireAfterSeconds: 90 }), 85, 'changed expiry gives IndexOptionsConflict', ); // expireAfterSeconds 0 is legal: expire at exactly the stored instant. const zero = db.collection('zero'); await zero.drop().catch(() => {}); await zero.createIndex({ at: 1 }, { expireAfterSeconds: 0 }); await zero.insertOne({ _id: 'now', at: new Date(Date.now() - 1000) }); check('expireAfterSeconds 0 accepted', (await zero.indexes()).some((i) => Number(i.expireAfterSeconds) === 0)); check('expireAfterSeconds 0 expires', await waitFor(async () => (await zero.countDocuments({})) === 0)); await client.close(); const failed = results.filter((r) => !r.ok); console.log(`\n${results.length - failed.length}/${results.length} checks passed`); if (failed.length) { console.log('FAILED:', failed.map((f) => f.name).join(', ')); process.exit(1); } console.log('E2E4_OK'); } main().catch((e) => { console.error('E2E4_FAIL', e); process.exit(1); });