Advertising topologyVersion in the hello reply is what tells a driver the server speaks the streaming (awaitable) hello protocol — in the Node driver it is the only condition checked. From the second heartbeat on, the driver then monitored with an exhaust hello (exhaustAllowed + maxAwaitTimeMS) and waited for a stream of replies carrying moreToCome. We answered once with the flag clear and went back to reading, so every heartbeat failed with "Server ended moreToCome unexpectedly", destroying the connection and clearing the pool. MongoDB Compass showed this as a connect/disconnect loop once per heartbeat. We do not implement streaming hello, so we must not claim to. Omitting the field keeps monitoring on the polling path, and agrees with the maxWireVersion 8 we report: streaming hello arrived in wire version 9. The existing e2e files all passed against the broken server — they issue their commands and exit before the second heartbeat — so e2e5 watches SDAM heartbeats on an idle connection instead. Also renames mongo-light to mongo-lite throughout (binary, log messages, docs, gitVersion). Unrelated to the fix above, but squashed in at request rather than left as a commit whose message described only the fix.
133 lines
5.5 KiB
JavaScript
133 lines
5.5 KiB
JavaScript
// End-to-end test: official MongoDB Node.js driver against mongo-lite.
|
|
const { MongoClient, ObjectId } = 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}`);
|
|
}
|
|
async function main() {
|
|
const client = new MongoClient(URL, { serverSelectionTimeoutMS: 5000 });
|
|
await client.connect();
|
|
const db = client.db('e2e');
|
|
const users = db.collection('users');
|
|
await users.drop().catch(() => {});
|
|
|
|
// --- insert ---
|
|
await users.insertOne({ name: 'alice', age: 30, tags: ['a', 'b'] });
|
|
const many = await users.insertMany([
|
|
{ name: 'bob', age: 25, tags: ['b'] },
|
|
{ name: 'carol', age: 35, tags: ['c', 'a'] },
|
|
{ name: 'dave', age: 40, tags: [] },
|
|
]);
|
|
check('insertMany acknowledged', many.acknowledged === true, many);
|
|
check('auto _id assigned', ObjectId.isValid(many.insertedIds[0]));
|
|
|
|
// --- find: filters, operators, sort, skip, limit, projection ---
|
|
const gt = await users.find({ age: { $gt: 28 } }).sort({ age: -1 }).toArray();
|
|
check('find $gt + sort desc', gt.map((d) => d.name).join(',') === 'dave,carol,alice', gt.map((d) => d.name));
|
|
|
|
const inq = await users.find({ name: { $in: ['alice', 'bob'] } }).count();
|
|
check('find $in count', inq === 2, inq);
|
|
|
|
const rgx = await users.find({ name: /^[bc]/ }).toArray();
|
|
check('find $regex', rgx.length === 2, rgx.map((d) => d.name));
|
|
|
|
const exists = await users.find({ tags: { $exists: true } }).count();
|
|
check('find $exists', exists === 4, exists);
|
|
|
|
const lim = await users.find({}).sort({ age: 1 }).skip(1).limit(2).toArray();
|
|
check('find skip+limit+sort', lim.map((d) => d.name).join(',') === 'alice,carol', lim.map((d) => d.name));
|
|
|
|
const proj = await users.findOne({ name: 'alice' }, { projection: { _id: 0, name: 1 } });
|
|
check('projection', proj.name === 'alice' && proj.age === undefined, JSON.stringify(proj));
|
|
|
|
const dot = await users.findOne({ 'tags.0': 'c' });
|
|
check('dot path + array', dot?.name === 'carol');
|
|
|
|
// --- count ---
|
|
check('countDocuments', (await users.countDocuments({})) === 4);
|
|
check('countDocuments with filter', (await users.countDocuments({ age: { $gte: 30 } })) === 3);
|
|
check('estimatedDocumentCount', (await users.estimatedDocumentCount()) === 4);
|
|
|
|
// --- update ---
|
|
const u1 = await users.updateOne({ name: 'alice' }, { $set: { vip: true }, $inc: { age: 1 } });
|
|
check('updateOne nModified', u1.modifiedCount === 1, u1);
|
|
const alice = await users.findOne({ name: 'alice' });
|
|
check('updateOne $set+$inc applied', alice.vip === true && alice.age === 31, JSON.stringify(alice));
|
|
|
|
const um = await users.updateMany({}, { $set: { seen: true } });
|
|
check('updateMany', um.modifiedCount === 4, um);
|
|
|
|
const push = await users.updateOne({ name: 'dave' }, { $push: { tags: 'x' } });
|
|
check('$push', push.modifiedCount === 1);
|
|
check('$push visible', (await users.findOne({ name: 'dave' })).tags.length === 1);
|
|
|
|
const ups = await users.updateOne({ name: 'erin' }, { $set: { age: 28 } }, { upsert: true });
|
|
check('upsert', ups.upsertedCount === 1 && ups.matchedCount === 0, ups);
|
|
check('upsert doc exists', (await users.findOne({ name: 'erin' }))?.age === 28);
|
|
|
|
// --- findOneAndUpdate (findAndModify) ---
|
|
const fam = await users.findOneAndUpdate(
|
|
{ name: 'bob' },
|
|
{ $set: { lucky: true } },
|
|
{ returnDocument: 'after' },
|
|
);
|
|
const famDoc = fam.value ?? fam;
|
|
check('findOneAndUpdate returns new', famDoc.lucky === true, JSON.stringify(fam));
|
|
const famRemove = await users.findOneAndDelete({ name: 'erin' });
|
|
const famDelDoc = famRemove.value ?? famRemove;
|
|
check('findOneAndDelete', famDelDoc?.name === 'erin', JSON.stringify(famRemove));
|
|
|
|
// --- aggregate ---
|
|
const grp = await users
|
|
.aggregate([
|
|
{ $match: { age: { $gte: 25 } } },
|
|
{ $group: { _id: '$tags.length', total: { $sum: '$age' } } },
|
|
{ $sort: { _id: 1 } },
|
|
])
|
|
.toArray();
|
|
check('aggregate $match+$group+$sum+$sort', grp.length >= 1 && grp.some((g) => g.total > 0), JSON.stringify(grp));
|
|
|
|
const cnt = await users.aggregate([{ $match: {} }, { $count: 'n' }]).toArray();
|
|
check('aggregate $count', cnt[0]?.n === 4, JSON.stringify(cnt));
|
|
|
|
// --- duplicate key ---
|
|
let dupErr = null;
|
|
try {
|
|
await users.insertOne({ _id: many.insertedIds[0], name: 'clobber' });
|
|
} catch (e) {
|
|
dupErr = e;
|
|
}
|
|
check('duplicate key rejected', dupErr?.code === 11000, dupErr?.message);
|
|
|
|
// --- listCollections / listDatabases ---
|
|
const colls = await db.listCollections({}, { nameOnly: true }).toArray();
|
|
check('listCollections', colls.some((c) => c.name === 'users'), JSON.stringify(colls));
|
|
const dbs = await client.db('admin').admin().listDatabases();
|
|
check('listDatabases', dbs.databases.some((d) => d.name === 'e2e'), JSON.stringify(dbs.databases.map((d) => d.name)));
|
|
|
|
// --- delete ---
|
|
const del1 = await users.deleteOne({ name: 'dave' });
|
|
check('deleteOne', del1.deletedCount === 1, del1);
|
|
const delMany = await users.deleteMany({});
|
|
check('deleteMany', delMany.deletedCount === 3, delMany);
|
|
check('empty after delete', (await users.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('E2E_OK');
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error('E2E_FAIL', e);
|
|
process.exit(1);
|
|
});
|