'use strict';
const assert = require('assert');
const mongoose = require('../index');
const dc = require('node:diagnostics_channel');
function subscribe(channelName, handlers) {
const ch = dc.tracingChannel(channelName);
ch.subscribe(handlers);
return () => ch.unsubscribe(handlers);
}
describe('TracingChannel', function() {
let conn;
let Test;
let collectionName;
before(async function() {
conn = mongoose.createConnection(require('./common').uri);
await conn.asPromise();
const schema = new mongoose.Schema({
name: String,
age: Number
});
Test = conn.model('TracingTest', schema);
collectionName = Test.collection.collectionName;
});
after(async function() {
await Test.deleteMany({});
await conn.close();
});
afterEach(async function() {
await Test.deleteMany({});
});
describe('query operations', function() {
it('fires start and asyncEnd for find', async function() {
const events = [];
const handlers = {
start(ctx) { events.push({ event: 'start', ...ctx }); },
end() { events.push({ event: 'end' }); },
asyncStart(ctx) { events.push({ event: 'asyncStart', result: ctx.result }); },
asyncEnd(ctx) { events.push({ event: 'asyncEnd', result: ctx.result }); },
error(ctx) { events.push({ event: 'error', error: ctx.error }); }
};
const unsubscribe = subscribe('mongoose:query', handlers);
try {
await Test.find({ name: 'test' });
const start = events.find(e => e.event === 'start');
assert.ok(start, 'start event should fire');
assert.strictEqual(start.operation, 'find');
assert.strictEqual(start.collection, collectionName);
assert.ok(start.database);
assert.ok(start.serverAddress);
assert.deepStrictEqual(start.args.filter, { name: 'test' });
const asyncEnd = events.find(e => e.event === 'asyncEnd');
assert.ok(asyncEnd, 'asyncEnd should fire');
assert.ok(Array.isArray(asyncEnd.result), 'asyncEnd should include the result');
} finally {
unsubscribe();
}
});
it('fires start and asyncEnd for findOne', async function() {
const events = [];
const handlers = {
start(ctx) { events.push({ event: 'start', ...ctx }); },
end() {},
asyncStart() {},
asyncEnd() { events.push({ event: 'asyncEnd' }); },
error() {}
};
const unsubscribe = subscribe('mongoose:query', handlers);
try {
await Test.findOne({ name: 'test' });
const start = events.find(e => e.event === 'start');
assert.ok(start);
assert.strictEqual(start.operation, 'findOne');
assert.strictEqual(start.collection, collectionName);
} finally {
unsubscribe();
}
});
it('fires start and asyncEnd for updateOne', async function() {
await Test.create({ name: 'update-test', age: 10 });
const events = [];
const handlers = {
start(ctx) { events.push({ event: 'start', ...ctx }); },
end() {},
asyncStart() {},
asyncEnd() { events.push({ event: 'asyncEnd' }); },
error() {}
};
const unsubscribe = subscribe('mongoose:query', handlers);
try {
await Test.updateOne({ name: 'update-test' }, { age: 20 });
const start = events.find(e => e.event === 'start');
assert.ok(start);
assert.strictEqual(start.operation, 'updateOne');
assert.strictEqual(start.collection, collectionName);
} finally {
unsubscribe();
}
});
it('fires start and asyncEnd for deleteOne', async function() {
await Test.create({ name: 'delete-test' });
const events = [];
const handlers = {
start(ctx) { events.push({ event: 'start', ...ctx }); },
end() {},
asyncStart() {},
asyncEnd() { events.push({ event: 'asyncEnd' }); },
error() {}
};
const unsubscribe = subscribe('mongoose:query', handlers);
try {
await Test.deleteOne({ name: 'delete-test' });
const start = events.find(e => e.event === 'start');
assert.ok(start);
assert.strictEqual(start.operation, 'deleteOne');
} finally {
unsubscribe();
}
});
it('fires error event on query failure', async function() {
const events = [];
const handlers = {
start() { events.push({ event: 'start' }); },
end() {},
asyncStart() {},
asyncEnd() {},
error(ctx) { events.push({ event: 'error', error: ctx.error }); }
};
const unsubscribe = subscribe('mongoose:query', handlers);
try {
await Test.find({ $invalidOperator: true }).catch(() => {});
assert.ok(events.some(e => e.event === 'start'), 'start should fire');
assert.ok(events.some(e => e.event === 'error'), 'error should fire');
} finally {
unsubscribe();
}
});
it('includes connection info in context', async function() {
const events = [];
const handlers = {
start(ctx) { events.push(ctx); },
end() {},
asyncStart() {},
asyncEnd() {},
error() {}
};
const unsubscribe = subscribe('mongoose:query', handlers);
try {
await Test.find({});
const ctx = events[0];
assert.ok(ctx.database);
assert.ok(ctx.serverAddress);
} finally {
unsubscribe();
}
});
});
describe('aggregate operations', function() {
it('fires start and asyncEnd for aggregate', async function() {
await Test.create([{ name: 'agg1', age: 10 }, { name: 'agg2', age: 20 }]);
const events = [];
const handlers = {
start(ctx) { events.push({ event: 'start', ...ctx }); },
end() {},
asyncStart() {},
asyncEnd() { events.push({ event: 'asyncEnd' }); },
error() {}
};
const unsubscribe = subscribe('mongoose:aggregate', handlers);
try {
await Test.aggregate([{ $match: { age: { $gte: 10 } } }]);
const start = events.find(e => e.event === 'start');
assert.ok(start, 'start event should fire');
assert.strictEqual(start.operation, 'aggregate');
assert.strictEqual(start.collection, collectionName);
assert.ok(start.database);
assert.ok(Array.isArray(start.args.pipeline));
assert.deepStrictEqual(start.args.pipeline[0], { $match: { age: { $gte: 10 } } });
assert.ok(events.some(e => e.event === 'asyncEnd'));
} finally {
unsubscribe();
}
});
it('fires start and asyncEnd for connection-level aggregate', async function() {
await Test.create([{ name: 'conn-agg1', age: 10 }, { name: 'conn-agg2', age: 20 }]);
const events = [];
const handlers = {
start(ctx) { events.push({ event: 'start', ...ctx }); },
end() {},
asyncStart() {},
asyncEnd(ctx) { events.push({ event: 'asyncEnd', result: ctx.result }); },
error() {}
};
const unsubscribe = subscribe('mongoose:aggregate', handlers);
try {
await conn.aggregate([{ $documents: [{ x: 1 }] }]);
const start = events.find(e => e.event === 'start');
assert.ok(start, 'start event should fire');
assert.strictEqual(start.operation, 'aggregate');
assert.ok(start.database);
assert.ok(Array.isArray(start.args.pipeline));
const asyncEnd = events.find(e => e.event === 'asyncEnd');
assert.ok(asyncEnd, 'asyncEnd should fire');
assert.ok(Array.isArray(asyncEnd.result));
} finally {
unsubscribe();
}
});
});
describe('save operations', function() {
it('fires start and asyncEnd for save (insert)', async function() {
const events = [];
const handlers = {
start(ctx) { events.push({ event: 'start', ...ctx }); },
end() {},
asyncStart() {},
asyncEnd(ctx) { events.push({ event: 'asyncEnd', result: ctx.result }); },
error() {}
};
const unsubscribe = subscribe('mongoose:model:save', handlers);
try {
const doc = new Test({ name: 'save-test', age: 25 });
await doc.save();
const start = events.find(e => e.event === 'start');
assert.ok(start, 'start event should fire');
assert.strictEqual(start.operation, 'save');
assert.strictEqual(start.collection, collectionName);
assert.ok(start.database);
const asyncEnd = events.find(e => e.event === 'asyncEnd');
assert.ok(asyncEnd, 'asyncEnd should fire');
assert.ok(asyncEnd.result, 'asyncEnd should include the result');
assert.strictEqual(asyncEnd.result.name, 'save-test');
} finally {
unsubscribe();
}
});
it('fires start and asyncEnd for save (update)', async function() {
const doc = await Test.create({ name: 'save-update', age: 10 });
const events = [];
const handlers = {
start(ctx) { events.push({ event: 'start', ...ctx }); },
end() {},
asyncStart() {},
asyncEnd() { events.push({ event: 'asyncEnd' }); },
error() {}
};
const unsubscribe = subscribe('mongoose:model:save', handlers);
try {
doc.age = 20;
await doc.save();
const start = events.find(e => e.event === 'start');
assert.ok(start);
assert.strictEqual(start.operation, 'save');
assert.strictEqual(start.collection, collectionName);
assert.ok(events.some(e => e.event === 'asyncEnd'));
} finally {
unsubscribe();
}
});
it('fires error event on save validation failure', async function() {
const schema = new mongoose.Schema({
email: { type: String, required: true }
});
const StrictModel = conn.model('TracingStrictTest', schema);
const events = [];
const handlers = {
start() { events.push({ event: 'start' }); },
end() {},
asyncStart() {},
asyncEnd() {},
error(ctx) { events.push({ event: 'error', error: ctx.error }); }
};
const unsubscribe = subscribe('mongoose:model:save', handlers);
try {
const doc = new StrictModel({});
await doc.save().catch(() => {});
assert.ok(events.some(e => e.event === 'start'), 'start should fire');
assert.ok(events.some(e => e.event === 'error'), 'error should fire');
} finally {
unsubscribe();
await StrictModel.deleteMany({});
}
});
});
describe('model operations', function() {
it('fires start and asyncEnd for insertMany', async function() {
const events = [];
const handlers = {
start(ctx) { events.push({ event: 'start', ...ctx }); },
end() {},
asyncStart() {},
asyncEnd() { events.push({ event: 'asyncEnd' }); },
error() {}
};
const unsubscribe = subscribe('mongoose:model:insertMany', handlers);
try {
await Test.insertMany([
{ name: 'insert1', age: 10 },
{ name: 'insert2', age: 20 }
]);
const start = events.find(e => e.event === 'start');
assert.ok(start, 'start event should fire');
assert.strictEqual(start.operation, 'insertMany');
assert.strictEqual(start.collection, collectionName);
assert.ok(start.database);
assert.ok(Array.isArray(start.args.docs));
assert.ok(events.some(e => e.event === 'asyncEnd'));
} finally {
unsubscribe();
}
});
it('fires start and asyncEnd for bulkWrite', async function() {
const events = [];
const handlers = {
start(ctx) { events.push({ event: 'start', ...ctx }); },
end() {},
asyncStart() {},
asyncEnd() { events.push({ event: 'asyncEnd' }); },
error() {}
};
const unsubscribe = subscribe('mongoose:model:bulkWrite', handlers);
try {
await Test.bulkWrite([
{ insertOne: { document: { name: 'bulk1', age: 30 } } }
]);
const start = events.find(e => e.event === 'start');
assert.ok(start, 'start event should fire');
assert.strictEqual(start.operation, 'bulkWrite');
assert.strictEqual(start.collection, collectionName);
assert.ok(start.database);
assert.ok(Array.isArray(start.args.ops));
assert.ok(events.some(e => e.event === 'asyncEnd'));
} finally {
unsubscribe();
}
});
});
describe('cursor:next operations', function() {
it('fires start and asyncEnd for query cursor next()', async function() {
await Test.create([
{ name: 'cursor1', age: 10 },
{ name: 'cursor2', age: 20 }
]);
const events = [];
const handlers = {
start(ctx) { events.push({ event: 'start', ...ctx }); },
end() { events.push({ event: 'end' }); },
asyncStart(ctx) { events.push({ event: 'asyncStart', result: ctx.result }); },
asyncEnd(ctx) { events.push({ event: 'asyncEnd', result: ctx.result }); },
error(ctx) { events.push({ event: 'error', error: ctx.error }); }
};
const unsubscribe = subscribe('mongoose:cursor:next', handlers);
try {
const cursor = Test.find({ name: /^cursor/ }).sort({ name: 1 }).cursor();
const doc1 = await cursor.next();
const doc2 = await cursor.next();
const doc3 = await cursor.next();
assert.strictEqual(doc1.name, 'cursor1');
assert.strictEqual(doc2.name, 'cursor2');
assert.strictEqual(doc3, null);
const starts = events.filter(e => e.event === 'start');
assert.strictEqual(starts.length, 3, 'should fire start for each next() call');
assert.strictEqual(starts[0].operation, 'find');
assert.strictEqual(starts[0].collection, collectionName);
assert.ok(starts[0].database);
assert.strictEqual(starts[0].tailable, false);
const asyncEnds = events.filter(e => e.event === 'asyncEnd');
assert.strictEqual(asyncEnds.length, 3, 'should fire asyncEnd for each next() call');
} finally {
unsubscribe();
}
});
it('includes batchSize in query cursor context', async function() {
await Test.create([
{ name: 'batch1', age: 10 },
{ name: 'batch2', age: 20 }
]);
const events = [];
const handlers = {
start(ctx) { events.push({ event: 'start', ...ctx }); },
end() {},
asyncStart() {},
asyncEnd() {},
error() {}
};
const unsubscribe = subscribe('mongoose:cursor:next', handlers);
try {
const cursor = Test.find({ name: /^batch/ }).cursor({ batchSize: 10 });
await cursor.next();
const start = events.find(e => e.event === 'start');
assert.strictEqual(start.batchSize, 10);
assert.strictEqual(start.tailable, false);
} finally {
unsubscribe();
}
});
it('fires start and asyncEnd for aggregate cursor next()', async function() {
await Test.create([
{ name: 'agg-cursor1', age: 10 },
{ name: 'agg-cursor2', age: 20 }
]);
const events = [];
const handlers = {
start(ctx) { events.push({ event: 'start', ...ctx }); },
end() { events.push({ event: 'end' }); },
asyncStart() {},
asyncEnd(ctx) { events.push({ event: 'asyncEnd', result: ctx.result }); },
error() {}
};
const unsubscribe = subscribe('mongoose:cursor:next', handlers);
try {
const cursor = Test.aggregate([
{ $match: { name: /^agg-cursor/ } },
{ $sort: { name: 1 } }
]).cursor();
const doc1 = await cursor.next();
const doc2 = await cursor.next();
const doc3 = await cursor.next();
assert.strictEqual(doc1.name, 'agg-cursor1');
assert.strictEqual(doc2.name, 'agg-cursor2');
assert.strictEqual(doc3, null);
const starts = events.filter(e => e.event === 'start');
assert.strictEqual(starts.length, 3, 'should fire start for each next() call');
assert.strictEqual(starts[0].operation, 'aggregate');
assert.strictEqual(starts[0].collection, collectionName);
assert.ok(starts[0].database);
assert.ok(Array.isArray(starts[0].args.pipeline));
} finally {
unsubscribe();
}
});
it('fires error event on cursor next() failure', async function() {
const events = [];
const handlers = {
start() { events.push({ event: 'start' }); },
end() {},
asyncStart() {},
asyncEnd() {},
error(ctx) { events.push({ event: 'error', error: ctx.error }); }
};
const unsubscribe = subscribe('mongoose:cursor:next', handlers);
try {
const cursor = Test.find({ $invalidOperator: true }).cursor();
await cursor.next().catch(() => {});
assert.ok(events.some(e => e.event === 'start'), 'start should fire');
assert.ok(events.some(e => e.event === 'error'), 'error should fire');
} finally {
unsubscribe();
}
});
it('does not fire cursor:next events when using regular find()', async function() {
await Test.create({ name: 'no-cursor', age: 5 });
const events = [];
const handlers = {
start() { events.push({ event: 'start' }); },
end() {},
asyncStart() {},
asyncEnd() {},
error() {}
};
const unsubscribe = subscribe('mongoose:cursor:next', handlers);
try {
await Test.find({ name: 'no-cursor' });
assert.strictEqual(events.length, 0, 'cursor:next should not fire for regular find()');
} finally {
unsubscribe();
}
});
});
describe('zero-cost when no subscribers', function() {
it('operations work without any subscribers', async function() {
const doc = new Test({ name: 'no-sub', age: 5 });
await doc.save();
const found = await Test.find({ name: 'no-sub' });
assert.strictEqual(found.length, 1);
await Test.aggregate([{ $match: { name: 'no-sub' } }]);
await Test.insertMany([{ name: 'no-sub-insert', age: 6 }]);
await Test.bulkWrite([
{ insertOne: { document: { name: 'no-sub-bulk', age: 7 } } }
]);
await Test.deleteMany({ name: /^no-sub/ });
});
it('cursor next() works without any subscribers', async function() {
await Test.create({ name: 'no-sub-cursor', age: 5 });
const cursor = Test.find({ name: 'no-sub-cursor' }).cursor();
const doc = await cursor.next();
assert.strictEqual(doc.name, 'no-sub-cursor');
const done = await cursor.next();
assert.strictEqual(done, null);
});
});
});