'use strict';
* Test dependencies.
*/
const { builtInMiddleware } = require('../lib/schema/symbols');
const start = require('./common');
const assert = require('assert');
const sinon = require('sinon');
const mongoose = start.mongoose;
const Schema = mongoose.Schema;
describe('middleware option to skip hooks (gh-8768)', function() {
let db;
before(function() {
db = start();
});
after(async function() {
await db.close();
});
beforeEach(() => db.deleteModel(/.*/));
afterEach(() => require('./util').clearTestData(db));
afterEach(() => require('./util').stopRemainingOps(db));
describe('Model and Query operations', function() {
const operations = {
find: (User, options) => User.find({}, null, options),
findOne: (User, options) => User.findOne({}, null, options),
findOneAndUpdate: (User, options) => User.findOneAndUpdate({}, { name: 'updated' }, options),
findOneAndDelete: (User, options) => User.findOneAndDelete({}, options),
findOneAndReplace: (User, options) => User.findOneAndReplace({}, { name: 'replaced' }, options),
updateOne: (User, options) => User.updateOne({}, { name: 'updated' }, options),
updateMany: (User, options) => User.updateMany({}, { name: 'updated' }, options),
deleteOne: (User, options) => User.deleteOne({}, options),
deleteMany: (User, options) => User.deleteMany({}, options),
countDocuments: (User, options) => User.countDocuments({}, options),
replaceOne: (User, options) => User.replaceOne({}, { name: 'replaced' }, options),
save: (User, options) => new User({ name: 'John' }).save(options),
validate: (User, options) => new User({ name: 'John' }).validate(options),
insertMany: (User, options) => User.insertMany([{ name: 'John' }], options),
bulkWrite: (User, options) => User.bulkWrite([{ insertOne: { document: { name: 'John' } } }], options),
aggregate: (User, options) => User.aggregate([{ $match: {} }], options),
distinct: (User, options) => User.distinct('name', {}, options),
estimatedDocumentCount: (User, options) => User.estimatedDocumentCount(options),
createCollection: (User, options) => User.createCollection(options)
};
for (const [operation, runOperation] of Object.entries(operations)) {
describe(operation, function() {
it('skips pre/post hooks when middleware: false', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await runOperation(User, { middleware: false });
assert.strictEqual(getPreCount(operation), 0);
assert.strictEqual(getPostCount(operation), 0);
});
it('runs hooks normally without middleware option', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await runOperation(User);
assert.strictEqual(getPreCount(operation), 1);
assert.strictEqual(getPostCount(operation), 1);
});
it('skips only pre hooks when middleware: { pre: false }', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await runOperation(User, { middleware: { pre: false } });
assert.strictEqual(getPreCount(operation), 0);
assert.strictEqual(getPostCount(operation), 1);
});
it('skips only post hooks when middleware: { post: false }', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await runOperation(User, { middleware: { post: false } });
assert.strictEqual(getPreCount(operation), 1);
assert.strictEqual(getPostCount(operation), 0);
});
});
}
describe('bulkSave()', function() {
it('skips pre/post hooks when middleware: false', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
const user = new User({ name: 'test' });
await User.bulkSave([user], { middleware: false });
assert.strictEqual(getPreCount('save'), 0);
assert.strictEqual(getPostCount('save'), 0);
});
it('skips only pre hooks when middleware: { pre: false }', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
const user = new User({ name: 'test' });
await User.bulkSave([user], { middleware: { pre: false } });
assert.strictEqual(getPreCount('save'), 0);
assert.strictEqual(getPostCount('save'), 1);
});
it('skips only post hooks when middleware: { post: false }', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
const user = new User({ name: 'test' });
await User.bulkSave([user], { middleware: { post: false } });
assert.strictEqual(getPreCount('save'), 1);
assert.strictEqual(getPostCount('save'), 0);
});
});
});
describe('aggregate().explain()', function() {
it('skips pre/post hooks when middleware: false', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.aggregate([{ $match: {} }]).option({ middleware: false }).explain();
assert.strictEqual(getPreCount('aggregate'), 0);
assert.strictEqual(getPostCount('aggregate'), 0);
});
it('runs hooks normally without middleware option', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.aggregate([{ $match: {} }]).explain();
assert.strictEqual(getPreCount('aggregate'), 1);
assert.strictEqual(getPostCount('aggregate'), 1);
});
it('skips only pre hooks when middleware: { pre: false }', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.aggregate([{ $match: {} }]).option({ middleware: { pre: false } }).explain();
assert.strictEqual(getPreCount('aggregate'), 0);
assert.strictEqual(getPostCount('aggregate'), 1);
});
it('skips only post hooks when middleware: { post: false }', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.aggregate([{ $match: {} }]).option({ middleware: { post: false } }).explain();
assert.strictEqual(getPreCount('aggregate'), 1);
assert.strictEqual(getPostCount('aggregate'), 0);
});
});
describe('init hooks via query options', function() {
it('skips pre/post init hooks when middleware: false', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'test' });
await User.findOne({}, null, { middleware: false });
assert.strictEqual(getPreCount('findOne'), 0);
assert.strictEqual(getPostCount('findOne'), 0);
assert.strictEqual(getPreCount('init'), 0);
assert.strictEqual(getPostCount('init'), 0);
});
it('runs init hooks normally without middleware option', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'test' });
await User.findOne({});
assert.strictEqual(getPreCount('findOne'), 1);
assert.strictEqual(getPostCount('findOne'), 1);
assert.strictEqual(getPreCount('init'), 1);
assert.strictEqual(getPostCount('init'), 1);
});
it('skips only pre init hooks when middleware: { pre: false }', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'test' });
await User.findOne({}, null, { middleware: { pre: false } });
assert.strictEqual(getPreCount('findOne'), 0);
assert.strictEqual(getPostCount('findOne'), 1);
assert.strictEqual(getPreCount('init'), 0);
assert.strictEqual(getPostCount('init'), 1);
});
it('skips only post init hooks when middleware: { post: false }', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'test' });
await User.findOne({}, null, { middleware: { post: false } });
assert.strictEqual(getPreCount('findOne'), 1);
assert.strictEqual(getPostCount('findOne'), 0);
assert.strictEqual(getPreCount('init'), 1);
assert.strictEqual(getPostCount('init'), 0);
});
});
describe('Subdocument operations', function() {
describe('save hooks', function() {
describe('subdocument.save()', function() {
it('skips pre/post hooks when middleware: false', async function() {
const { User, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = new User({ name: 'test', address: { city: 'NYC' } });
await user.address.save({ suppressWarning: true, middleware: false });
assert.strictEqual(getSubdocPreCount('save'), 0);
assert.strictEqual(getSubdocPostCount('save'), 0);
});
it('runs hooks normally without middleware option', async function() {
const { User, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = new User({ name: 'test', address: { city: 'NYC' } });
await user.address.save({ suppressWarning: true });
assert.strictEqual(getSubdocPreCount('save'), 1);
assert.strictEqual(getSubdocPostCount('save'), 1);
});
it('skips only pre hooks when middleware: { pre: false }', async function() {
const { User, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = new User({ name: 'test', address: { city: 'NYC' } });
await user.address.save({ suppressWarning: true, middleware: { pre: false } });
assert.strictEqual(getSubdocPreCount('save'), 0);
assert.strictEqual(getSubdocPostCount('save'), 1);
});
it('skips only post hooks when middleware: { post: false }', async function() {
const { User, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = new User({ name: 'test', address: { city: 'NYC' } });
await user.address.save({ suppressWarning: true, middleware: { post: false } });
assert.strictEqual(getSubdocPreCount('save'), 1);
assert.strictEqual(getSubdocPostCount('save'), 0);
});
});
describe('parent.save() with subdocs', function() {
it('skips both parent and subdoc middleware when middleware: false', async function() {
const { User, getPreCount, getPostCount, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = new User({ name: 'test', address: { city: 'NYC' } });
await user.save({ middleware: false });
assert.strictEqual(getPreCount('save'), 0);
assert.strictEqual(getPostCount('save'), 0);
assert.strictEqual(getSubdocPreCount('save'), 0);
assert.strictEqual(getSubdocPostCount('save'), 0);
});
it('runs hooks normally without middleware option', async function() {
const { User, getPreCount, getPostCount, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = new User({ name: 'test', address: { city: 'NYC' } });
await user.save();
assert.strictEqual(getPreCount('save'), 1);
assert.strictEqual(getPostCount('save'), 1);
assert.strictEqual(getSubdocPreCount('save'), 1);
assert.strictEqual(getSubdocPostCount('save'), 1);
});
it('skips only pre middleware for parent and subdocs when middleware: { pre: false }', async function() {
const { User, getPreCount, getPostCount, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = new User({ name: 'test', address: { city: 'NYC' } });
await user.save({ middleware: { pre: false } });
assert.strictEqual(getPreCount('save'), 0);
assert.strictEqual(getPostCount('save'), 1);
assert.strictEqual(getSubdocPreCount('save'), 0);
assert.strictEqual(getSubdocPostCount('save'), 1);
});
it('skips only post middleware for parent and subdocs when middleware: { post: false }', async function() {
const { User, getPreCount, getPostCount, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = new User({ name: 'test', address: { city: 'NYC' } });
await user.save({ middleware: { post: false } });
assert.strictEqual(getPreCount('save'), 1);
assert.strictEqual(getPostCount('save'), 0);
assert.strictEqual(getSubdocPreCount('save'), 1);
assert.strictEqual(getSubdocPostCount('save'), 0);
});
});
});
});
describe('deleteOne hooks on removed subdocs', function() {
it('parent.save() skips subdoc deleteOne hooks when middleware: false', async function() {
const { User, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = await User.create({ name: 'parent', posts: [{ title: 'First post' }] });
user.posts[0].deleteOne();
await user.save({ middleware: false });
assert.strictEqual(getSubdocPreCount('deleteOne'), 0);
assert.strictEqual(getSubdocPostCount('deleteOne'), 0);
});
it('parent.save() skips only pre deleteOne hooks when middleware: { pre: false }', async function() {
const { User, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = await User.create({ name: 'parent', posts: [{ title: 'First post' }] });
user.posts[0].deleteOne();
await user.save({ middleware: { pre: false } });
assert.strictEqual(getSubdocPreCount('deleteOne'), 0);
assert.strictEqual(getSubdocPostCount('deleteOne'), 1);
});
it('parent.save() skips only post deleteOne hooks when middleware: { post: false }', async function() {
const { User, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = await User.create({ name: 'parent', posts: [{ title: 'First post' }] });
user.posts[0].deleteOne();
await user.save({ middleware: { post: false } });
assert.strictEqual(getSubdocPreCount('deleteOne'), 1);
assert.strictEqual(getSubdocPostCount('deleteOne'), 0);
});
});
describe('Document instance operations', function() {
const operations = {
updateOne: (user, options) => user.updateOne({ name: 'updated' }, options),
deleteOne: (user, options) => user.deleteOne(options)
};
for (const [operation, runOperation] of Object.entries(operations)) {
describe(`doc.${operation}()`, function() {
it('skips pre/post hooks when middleware: false', async function() {
const { User, getDocPreCount, getDocPostCount } = createTestContext();
const user = await User.create({ name: 'test' });
await runOperation(user, { middleware: false });
assert.strictEqual(getDocPreCount(operation), 0);
assert.strictEqual(getDocPostCount(operation), 0);
});
it('runs hooks normally without middleware option', async function() {
const { User, getDocPreCount, getDocPostCount } = createTestContext();
const user = await User.create({ name: 'test' });
await runOperation(user);
assert.strictEqual(getDocPreCount(operation), 1);
assert.strictEqual(getDocPostCount(operation), 1);
});
it('skips only pre hooks when middleware: { pre: false }', async function() {
const { User, getDocPreCount, getDocPostCount } = createTestContext();
const user = await User.create({ name: 'test' });
await runOperation(user, { middleware: { pre: false } });
assert.strictEqual(getDocPreCount(operation), 0);
assert.strictEqual(getDocPostCount(operation), 1);
});
it('skips only post hooks when middleware: { post: false }', async function() {
const { User, getDocPreCount, getDocPostCount } = createTestContext();
const user = await User.create({ name: 'test' });
await runOperation(user, { middleware: { post: false } });
assert.strictEqual(getDocPreCount(operation), 1);
assert.strictEqual(getDocPostCount(operation), 0);
});
});
}
describe('doc.deleteOne() with subdocs', function() {
it('skips subdoc deleteOne hooks when middleware: false', async function() {
const { User, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = await User.create({ name: 'parent', posts: [{ title: 'First post' }] });
await user.deleteOne({ middleware: false });
assert.strictEqual(getSubdocPreCount('deleteOne'), 0);
assert.strictEqual(getSubdocPostCount('deleteOne'), 0);
});
it('runs subdoc deleteOne hooks normally without middleware option', async function() {
const { User, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = await User.create({ name: 'parent', posts: [{ title: 'First post' }] });
await user.deleteOne();
assert.strictEqual(getSubdocPreCount('deleteOne'), 1);
assert.strictEqual(getSubdocPostCount('deleteOne'), 1);
});
it('skips only subdoc pre deleteOne hooks when middleware: { pre: false }', async function() {
const { User, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = await User.create({ name: 'parent', posts: [{ title: 'First post' }] });
await user.deleteOne({ middleware: { pre: false } });
assert.strictEqual(getSubdocPreCount('deleteOne'), 0);
assert.strictEqual(getSubdocPostCount('deleteOne'), 1);
});
it('skips only subdoc post deleteOne hooks when middleware: { post: false }', async function() {
const { User, getSubdocPreCount, getSubdocPostCount } = createTestContext();
const user = await User.create({ name: 'parent', posts: [{ title: 'First post' }] });
await user.deleteOne({ middleware: { post: false } });
assert.strictEqual(getSubdocPreCount('deleteOne'), 1);
assert.strictEqual(getSubdocPostCount('deleteOne'), 0);
});
});
});
describe('Built-in middleware still runs when user middleware is skipped', function() {
it('save() sets timestamps when middleware: false', async function() {
const { User, getUserHookRan } = createTestContext();
const user = new User({ name: 'test' });
await user.save({ middleware: false });
assert.strictEqual(getUserHookRan('save'), false);
assert.ok(user.createdAt);
assert.ok(user.updatedAt);
});
it('findOneAndUpdate() updates timestamps when middleware: false', async function() {
const { User, getUserHookRan } = createTestContext();
const user = await User.create({ name: 'test' });
const originalUpdatedAt = user.updatedAt;
await new Promise(resolve => setTimeout(resolve, 1));
const updated = await User.findOneAndUpdate(
{ _id: user._id },
{ name: 'updated' },
{ returnDocument: 'after', middleware: false }
);
assert.strictEqual(getUserHookRan('findOneAndUpdate'), false);
assert.ok(updated.updatedAt > originalUpdatedAt);
});
it('bulkSave() sets timestamps when middleware: false', async function() {
const { User, getUserHookRan } = createTestContext();
const user = new User({ name: 'test' });
await User.bulkSave([user], { middleware: false });
assert.strictEqual(getUserHookRan('save'), false);
assert.ok(user.createdAt);
assert.ok(user.updatedAt);
});
it('save() skips user hooks on subdocuments when middleware: false', async function() {
const { User, getUserHookRan, getChildHookRan } = createTestContext();
const user = new User({ name: 'test', address: { city: 'NYC' } });
await user.save({ middleware: false });
assert.strictEqual(getUserHookRan('save'), false);
assert.strictEqual(getChildHookRan('save'), false);
});
it('save() still triggers subdoc save hooks mechanism when middleware: false', async function() {
const { User, getChildHookRan } = createTestContext();
const user = new User({ name: 'test', address: { city: 'NYC' } });
await user.save({ middleware: false });
assert.strictEqual(getChildHookRan('save'), false);
assert.ok(user.address.createdAt, 'Subdoc createdAt should be set');
assert.ok(user.address.updatedAt, 'Subdoc updatedAt should be set');
});
it('save() runs validation when middleware: false', async function() {
const { User, getUserHookRan } = createTestContext();
const user = new User({});
const err = await user.save({ middleware: false }).then(() => null, err => err);
assert.strictEqual(getUserHookRan('save'), false);
assert.ok(err);
assert.strictEqual(err.name, 'ValidationError');
});
it('all internal plugins have builtInMiddleware symbol', function() {
const userSchema = new Schema({ name: String }, { timestamps: true, shardKey: { name: 1 } });
db.model('Test', userSchema);
const pres = Array.from(userSchema.s.hooks._pres.values()).flat();
const posts = Array.from(userSchema.s.hooks._posts.values()).flat();
const allHooks = [...pres, ...posts];
const builtInHooks = allHooks.filter(hook => hook.fn[builtInMiddleware]);
assert.ok(builtInHooks.length >= 17, 'Expected at least 17 built-in hooks');
assert.deepStrictEqual(
allHooks,
builtInHooks,
'All internal plugin hooks should have builtInMiddleware symbol'
);
});
});
describe('middleware option parsing', function() {
it('runs hooks for falsy middleware option values other than false', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'John' });
await User.find({}, null, { middleware: 0 }).exec();
await User.find({}, null, { middleware: { pre: 0, post: '' } }).exec();
assert.strictEqual(getPreCount('find'), 2);
assert.strictEqual(getPostCount('find'), 2);
});
});
describe('query cursor', function() {
it('skips pre/post find hooks when middleware: false', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'John' });
let numDocs = 0;
await User.find().cursor({ middleware: false }).eachAsync(doc => {
++numDocs;
assert.strictEqual(doc.name, 'John');
});
assert.strictEqual(numDocs, 1);
assert.strictEqual(getPreCount('find'), 0);
assert.strictEqual(getPostCount('find'), 0);
});
it('runs find hooks normally without middleware option', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'John' });
let numDocs = 0;
await User.find().cursor().eachAsync(doc => {
++numDocs;
assert.strictEqual(doc.name, 'John');
});
assert.strictEqual(numDocs, 1);
assert.strictEqual(getPreCount('find'), 1);
assert.strictEqual(getPostCount('find'), 1);
});
it('skips only pre find hooks when middleware: { pre: false }', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'John' });
let numDocs = 0;
await User.find().cursor({ middleware: { pre: false } }).eachAsync(doc => {
++numDocs;
assert.strictEqual(doc.name, 'John');
});
assert.strictEqual(numDocs, 1);
assert.strictEqual(getPreCount('find'), 0);
assert.strictEqual(getPostCount('find'), 1);
});
it('skips only post find hooks when middleware: { post: false }', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'John' });
let numDocs = 0;
await User.find().cursor({ middleware: { post: false } }).eachAsync(doc => {
++numDocs;
assert.strictEqual(doc.name, 'John');
});
assert.strictEqual(numDocs, 1);
assert.strictEqual(getPreCount('find'), 1);
assert.strictEqual(getPostCount('find'), 0);
});
it('skips pre/post find hooks when iterating with next() when middleware: false', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'John' });
const cursor = User.find().cursor({ middleware: false });
const doc = await cursor.next();
assert.ok(doc);
assert.strictEqual(doc.name, 'John');
assert.strictEqual(getPreCount('find'), 0);
assert.strictEqual(getPostCount('find'), 0);
});
it('does not leak the middleware option into collection.find', async function() {
const { User } = createTestContext();
await User.create({ name: 'John' });
const findSpy = sinon.spy(User.collection, 'find');
try {
await User.find().cursor({ middleware: false }).eachAsync(() => {});
} finally {
findSpy.restore();
}
assert.ok(findSpy.called);
assert.strictEqual(findSpy.firstCall.args[1].middleware, undefined);
});
});
describe('aggregate cursor', function() {
it('skips pre aggregate hooks when middleware: false', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'John' });
let numDocs = 0;
await User.aggregate([{ $match: {} }]).option({ middleware: false }).cursor().eachAsync(doc => {
++numDocs;
assert.strictEqual(doc.name, 'John');
});
assert.strictEqual(numDocs, 1);
assert.strictEqual(getPreCount('aggregate'), 0);
assert.strictEqual(getPostCount('aggregate'), 0);
});
it('skips pre aggregate hooks when cursor options have middleware: false', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'John' });
let numDocs = 0;
await User.aggregate([{ $match: {} }]).cursor({ middleware: false }).eachAsync(doc => {
++numDocs;
assert.strictEqual(doc.name, 'John');
});
assert.strictEqual(numDocs, 1);
assert.strictEqual(getPreCount('aggregate'), 0);
assert.strictEqual(getPostCount('aggregate'), 0);
});
it('runs pre aggregate hooks without running post aggregate hooks', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'John' });
let numDocs = 0;
await User.aggregate([{ $match: {} }]).cursor().eachAsync(doc => {
++numDocs;
assert.strictEqual(doc.name, 'John');
});
assert.strictEqual(numDocs, 1);
assert.strictEqual(getPreCount('aggregate'), 1);
assert.strictEqual(getPostCount('aggregate'), 0);
});
it('skips pre aggregate hooks when middleware: { pre: false }', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'John' });
let numDocs = 0;
await User.aggregate([{ $match: {} }]).cursor({ middleware: { pre: false } }).eachAsync(doc => {
++numDocs;
assert.strictEqual(doc.name, 'John');
});
assert.strictEqual(numDocs, 1);
assert.strictEqual(getPreCount('aggregate'), 0);
assert.strictEqual(getPostCount('aggregate'), 0);
});
it('runs pre aggregate hooks when middleware: { post: false }', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'John' });
let numDocs = 0;
await User.aggregate([{ $match: {} }]).option({ middleware: { post: false } }).cursor().eachAsync(doc => {
++numDocs;
assert.strictEqual(doc.name, 'John');
});
assert.strictEqual(numDocs, 1);
assert.strictEqual(getPreCount('aggregate'), 1);
assert.strictEqual(getPostCount('aggregate'), 0);
});
it('does not leak the middleware option into collection.aggregate', async function() {
const { User } = createTestContext();
await User.create({ name: 'John' });
const aggregateSpy = sinon.spy(User.collection, 'aggregate');
try {
await User.aggregate([{ $match: {} }]).option({ middleware: false }).cursor().eachAsync(() => {});
await User.aggregate([{ $match: {} }]).cursor({ middleware: false }).eachAsync(() => {});
} finally {
aggregateSpy.restore();
}
assert.strictEqual(aggregateSpy.callCount, 2);
assert.strictEqual(aggregateSpy.firstCall.args[1].middleware, undefined);
assert.strictEqual(aggregateSpy.secondCall.args[1].middleware, undefined);
assert.strictEqual(aggregateSpy.secondCall.args[1].cursor.middleware, undefined);
});
it('preserves the middleware option on the aggregate after running a cursor', async function() {
const { User, getPreCount, getPostCount } = createTestContext();
await User.create({ name: 'John' });
const aggregate = User.aggregate([{ $match: {} }]).option({ middleware: false });
await aggregate.cursor().eachAsync(() => {});
assert.strictEqual(getPreCount('aggregate'), 0);
assert.strictEqual(getPostCount('aggregate'), 0);
assert.strictEqual(aggregate.options.middleware, false);
});
});
describe('custom statics and methods', function() {
describe('statics', function() {
it('skips pre/post hooks on a custom static when middleware: false', async function() {
const { User, getStaticPreCount, getStaticPostCount } = createStaticsContext({ supportsMiddlewareOption: true });
await User.create({ name: 'John' });
const user = await User.findByName('John', { middleware: false });
assert.ok(user);
assert.strictEqual(user.name, 'John');
assert.strictEqual(getStaticPreCount(), 0);
assert.strictEqual(getStaticPostCount(), 0);
});
it('ignores the middleware option when the static does not set supportsMiddlewareOption', async function() {
const { User, getStaticPreCount, getStaticPostCount } = createStaticsContext();
await User.create({ name: 'John' });
const user = await User.findByName('John', { middleware: false });
assert.ok(user);
assert.strictEqual(user.name, 'John');
assert.strictEqual(getStaticPreCount(), 1);
assert.strictEqual(getStaticPostCount(), 1);
});
it('runs custom static hooks normally without middleware option', async function() {
const { User, getStaticPreCount, getStaticPostCount } = createStaticsContext({ supportsMiddlewareOption: true });
await User.create({ name: 'John' });
const user = await User.findByName('John');
assert.ok(user);
assert.strictEqual(user.name, 'John');
assert.strictEqual(getStaticPreCount(), 1);
assert.strictEqual(getStaticPostCount(), 1);
});
it('runs static hooks for falsy middleware option values other than false', async function() {
const { User, getStaticPreCount, getStaticPostCount } = createStaticsContext({ supportsMiddlewareOption: true });
await User.create({ name: 'John' });
const user = await User.findByName('John', { middleware: 0 });
assert.ok(user);
assert.strictEqual(getStaticPreCount(), 1);
assert.strictEqual(getStaticPostCount(), 1);
});
it('skips only pre hooks when middleware: { pre: false }', async function() {
const { User, getStaticPreCount, getStaticPostCount } = createStaticsContext({ supportsMiddlewareOption: true });
await User.create({ name: 'John' });
const user = await User.findByName('John', { middleware: { pre: false } });
assert.ok(user);
assert.strictEqual(user.name, 'John');
assert.strictEqual(getStaticPreCount(), 0);
assert.strictEqual(getStaticPostCount(), 1);
});
it('skips only post hooks when middleware: { post: false }', async function() {
const { User, getStaticPreCount, getStaticPostCount } = createStaticsContext({ supportsMiddlewareOption: true });
await User.create({ name: 'John' });
const user = await User.findByName('John', { middleware: { post: false } });
assert.ok(user);
assert.strictEqual(user.name, 'John');
assert.strictEqual(getStaticPreCount(), 1);
assert.strictEqual(getStaticPostCount(), 0);
});
function createStaticsContext({ supportsMiddlewareOption = false } = {}) {
let preCount = 0;
let postCount = 0;
const userSchema = new Schema({ name: String });
userSchema.statics.findByName = function(name, options = {}) {
assert.ok(options);
return this.findOne({ name });
};
if (supportsMiddlewareOption) {
userSchema.statics.findByName.supportsMiddlewareOption = true;
}
userSchema.pre('findByName', function() { preCount++; });
userSchema.post('findByName', function() { postCount++; });
const User = db.model('User', userSchema);
return { User, getStaticPreCount: () => preCount, getStaticPostCount: () => postCount };
}
});
describe('methods', function() {
it('skips pre/post hooks on a custom method when middleware: false', async function() {
const { User, getMethodPreCount, getMethodPostCount } = createMethodsContext({ supportsMiddlewareOption: true });
const user = await User.create({ name: 'John' });
const result = await user.activate({ middleware: false });
assert.strictEqual(result, user);
assert.strictEqual(user.active, true);
assert.strictEqual(getMethodPreCount(), 0);
assert.strictEqual(getMethodPostCount(), 0);
});
it('ignores the middleware option when the method does not set supportsMiddlewareOption', async function() {
const { User, getMethodPreCount, getMethodPostCount } = createMethodsContext();
const user = await User.create({ name: 'John' });
const result = await user.activate({ middleware: false });
assert.strictEqual(result, user);
assert.strictEqual(user.active, true);
assert.strictEqual(getMethodPreCount(), 1);
assert.strictEqual(getMethodPostCount(), 1);
});
it('runs custom method hooks normally without middleware option', async function() {
const { User, getMethodPreCount, getMethodPostCount } = createMethodsContext({ supportsMiddlewareOption: true });
const user = await User.create({ name: 'John' });
const result = await user.activate();
assert.strictEqual(result, user);
assert.strictEqual(user.active, true);
assert.strictEqual(getMethodPreCount(), 1);
assert.strictEqual(getMethodPostCount(), 1);
});
it('runs method hooks for falsy middleware option values other than false', async function() {
const { User, getMethodPreCount, getMethodPostCount } = createMethodsContext({ supportsMiddlewareOption: true });
const user = await User.create({ name: 'John' });
const result = await user.activate({ middleware: 0 });
assert.strictEqual(result, user);
assert.strictEqual(getMethodPreCount(), 1);
assert.strictEqual(getMethodPostCount(), 1);
});
it('skips only pre hooks when middleware: { pre: false }', async function() {
const { User, getMethodPreCount, getMethodPostCount } = createMethodsContext({ supportsMiddlewareOption: true });
const user = await User.create({ name: 'John' });
const result = await user.activate({ middleware: { pre: false } });
assert.strictEqual(result, user);
assert.strictEqual(user.active, true);
assert.strictEqual(getMethodPreCount(), 0);
assert.strictEqual(getMethodPostCount(), 1);
});
it('skips only post hooks when middleware: { post: false }', async function() {
const { User, getMethodPreCount, getMethodPostCount } = createMethodsContext({ supportsMiddlewareOption: true });
const user = await User.create({ name: 'John' });
const result = await user.activate({ middleware: { post: false } });
assert.strictEqual(result, user);
assert.strictEqual(user.active, true);
assert.strictEqual(getMethodPreCount(), 1);
assert.strictEqual(getMethodPostCount(), 0);
});
function createMethodsContext({ supportsMiddlewareOption = false } = {}) {
let preCount = 0;
let postCount = 0;
const userSchema = new Schema({ name: String, active: Boolean });
userSchema.methods.activate = function(options = {}) {
assert.ok(options);
this.active = true;
return this;
};
if (supportsMiddlewareOption) {
userSchema.methods.activate.supportsMiddlewareOption = true;
}
userSchema.pre('activate', function() { preCount++; });
userSchema.post('activate', function() { postCount++; });
const User = db.model('User', userSchema);
return { User, getMethodPreCount: () => preCount, getMethodPostCount: () => postCount };
}
});
});
* Unified test context factory that creates a schema with all hooks registered.
* Tests can check counters for any hook type without needing separate factory functions.
*/
function createTestContext() {
const hookNames = [
'save', 'validate', 'find', 'findOne', 'findOneAndUpdate',
'findOneAndDelete', 'findOneAndReplace', 'updateOne', 'updateMany',
'deleteOne', 'deleteMany', 'countDocuments', 'replaceOne',
'insertMany', 'bulkWrite', 'aggregate', 'distinct', 'estimatedDocumentCount',
'createCollection', 'init'
];
const documentHookNames = ['deleteOne', 'updateOne'];
const subdocHookNames = ['save', 'validate', 'deleteOne'];
const counts = { query: {}, document: {}, subdoc: {} };
for (const hook of hookNames) {
counts.query[hook] = { pre: 0, post: 0 };
counts.subdoc[hook] = { pre: 0, post: 0 };
}
for (const hook of documentHookNames) {
counts.document[hook] = { pre: 0, post: 0 };
}
const addressSchema = new Schema({ city: String }, { timestamps: true });
const postSchema = new Schema({ title: String }, { timestamps: true });
const userSchema = new Schema({
name: { type: String, required: true },
bio: String,
address: addressSchema,
posts: [postSchema]
}, { timestamps: true, autoCreate: false });
for (const subdocSchema of [addressSchema, postSchema]) {
for (const hook of subdocHookNames) {
const opts = hook === 'deleteOne' ? { document: true, query: false } : undefined;
subdocSchema.pre(hook, opts, function() { counts.subdoc[hook].pre++; });
subdocSchema.post(hook, opts, function() { counts.subdoc[hook].post++; });
}
}
for (const hook of hookNames) {
userSchema.pre(hook, function() { counts.query[hook].pre++; });
userSchema.post(hook, function() { counts.query[hook].post++; });
}
for (const hook of documentHookNames) {
userSchema.pre(hook, { document: true, query: false }, function() { counts.document[hook].pre++; });
userSchema.post(hook, { document: true, query: false }, function() { counts.document[hook].post++; });
}
const User = db.model('User', userSchema);
return {
User,
counts,
getPreCount: (hook) => counts.query[hook]?.pre ?? 0,
getPostCount: (hook) => counts.query[hook]?.post ?? 0,
getDocPreCount: (hook) => counts.document[hook]?.pre ?? 0,
getDocPostCount: (hook) => counts.document[hook]?.post ?? 0,
getSubdocPreCount: (hook) => counts.subdoc[hook]?.pre ?? 0,
getSubdocPostCount: (hook) => counts.subdoc[hook]?.post ?? 0,
getUserHookRan: (hook) => (counts.query[hook]?.pre > 0) || (counts.query[hook]?.post > 0) || (counts.document[hook]?.pre > 0) || (counts.document[hook]?.post > 0),
getChildHookRan: (hook) => (counts.subdoc[hook]?.pre > 0) || (counts.subdoc[hook]?.post > 0)
};
}
});