diff --git a/global.d.ts b/global.d.ts index 145acdf2835..8f7b44b6187 100644 --- a/global.d.ts +++ b/global.d.ts @@ -15,7 +15,7 @@ declare global { apiVersion?: '1'; clientSideEncryption?: boolean; serverless?: 'forbid' | 'allow' | 'require'; - auth: 'enabled' | 'disabled'; + auth?: 'enabled' | 'disabled'; }; sessions?: { @@ -39,9 +39,15 @@ declare global { interface TestFunction { (title: string, metadata: MongoDBMetadataUI, fn: Mocha.Func): Mocha.Test; (title: string, metadata: MongoDBMetadataUI, fn: Mocha.AsyncFunc): Mocha.Test; + (title: string, metadataAndTest: MetadataAndTest): Mocha.Test; + (title: string, metadataAndTest: MetadataAndTest): Mocha.Test; + } - (title: string, testAndMetadata: MetadataAndTest): Mocha.Test; - (title: string, testAndMetadata: MetadataAndTest): Mocha.Test; + interface PendingTestFunction { + (title: string, metadata: MongoDBMetadataUI, fn: Mocha.Func): Mocha.Test; + (title: string, metadata: MongoDBMetadataUI, fn: Mocha.AsyncFunc): Mocha.Test; + (title: string, metadataAndTest: MetadataAndTest): Mocha.Test; + (title: string, metadataAndTest: MetadataAndTest): Mocha.Test; } interface Context { diff --git a/package-lock.json b/package-lock.json index e914c99a331..6fd15b50743 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,6 @@ "eslint-plugin-tsdoc": "^0.2.14", "express": "^4.17.3", "js-yaml": "^4.1.0", - "lodash.camelcase": "^4.3.0", "mocha": "^9.2.2", "mocha-sinon": "^2.1.2", "nyc": "^15.1.0", @@ -4745,12 +4744,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", - "dev": true - }, "node_modules/lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -11659,12 +11652,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", - "dev": true - }, "lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", diff --git a/package.json b/package.json index df58dfd4093..8650d6ff031 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "eslint-plugin-tsdoc": "^0.2.14", "express": "^4.17.3", "js-yaml": "^4.1.0", - "lodash.camelcase": "^4.3.0", "mocha": "^9.2.2", "mocha-sinon": "^2.1.2", "nyc": "^15.1.0", diff --git a/test/integration/change-streams/change_stream.test.js b/test/integration/change-streams/change_stream.test.ts similarity index 83% rename from test/integration/change-streams/change_stream.test.js rename to test/integration/change-streams/change_stream.test.ts index 33e474647b2..9fb568bfc19 100644 --- a/test/integration/change-streams/change_stream.test.js +++ b/test/integration/change-streams/change_stream.test.ts @@ -1,32 +1,35 @@ -'use strict'; -const assert = require('assert'); -const { Transform, PassThrough } = require('stream'); -const { delay, setupDatabase, withClient, withCursor } = require('../shared'); -const mock = require('../../tools/mongodb-mock/index'); -const { EventCollector, getSymbolFrom } = require('../../tools/utils'); -const { expect } = require('chai'); - -const sinon = require('sinon'); -const { Long, ReadPreference, MongoNetworkError } = require('../../../src'); - -const crypto = require('crypto'); -const { isHello } = require('../../../src/utils'); -const { skipBrokenAuthTestBeforeEachHook } = require('../../tools/runner/hooks/configuration'); - -function withChangeStream(dbName, collectionName, callback) { - if (arguments.length === 1) { - callback = dbName; - dbName = undefined; - } else if (arguments.length === 2) { - callback = collectionName; - collectionName = dbName; - dbName = undefined; - } - - dbName = dbName || 'changestream_integration_test'; - collectionName = collectionName || 'test'; - - return withClient((client, done) => { +import * as assert from 'assert'; +import { expect } from 'chai'; +import * as crypto from 'crypto'; +import * as sinon from 'sinon'; +import { PassThrough, Transform } from 'stream'; + +import { + ChangeStream, + ChangeStreamOptions, + Collection, + Long, + MongoClient, + MongoNetworkError, + ReadPreference +} from '../../../src'; +import { isHello } from '../../../src/utils'; +import * as mock from '../../tools/mongodb-mock/index'; +import { skipBrokenAuthTestBeforeEachHook } from '../../tools/runner/hooks/configuration'; +import { EventCollector, getSymbolFrom } from '../../tools/utils'; +import { delay, setupDatabase, withClient, withCursor } from '../shared'; + +function withChangeStream( + callback: (collection: Collection, changeStream: ChangeStream, done: Mocha.Done) => void +): Mocha.Func { + const dbName = 'changestream_integration_test'; + const collectionName = 'test'; + + // TODO(NODE-2764): remove withClient and withChangeStream usage, use mocha hooks + const withClientTyped: (cb: (client: MongoClient, done: Mocha.Done) => void) => Mocha.Func = + withClient as any; + + return withClientTyped((client, done) => { const db = client.db(dbName); db.dropCollection(collectionName, () => { db.createCollection( @@ -47,13 +50,18 @@ function withChangeStream(dbName, collectionName, callback) { /** * Triggers a fake resumable error on a change stream - * - * @param {ChangeStream} changeStream - * @param {number} [delay] optional delay before triggering error - * @param {Function} onClose callback when cursor closed due this error + * changeStream + * [delay] optional delay before triggering error + * onClose callback when cursor closed due this error */ -function triggerResumableError(changeStream, delay, onClose) { - if (arguments.length === 2) { +function triggerResumableError(changeStream: ChangeStream, onClose?: () => void); +function triggerResumableError(changeStream: ChangeStream, delay: number, onClose?: () => void); +function triggerResumableError( + changeStream: ChangeStream, + delay: number | (() => void), + onClose?: () => void +) { + if (typeof delay === 'function') { onClose = delay; delay = undefined; } @@ -77,10 +85,12 @@ function triggerResumableError(changeStream, delay, onClose) { nextStub.restore(); }); - changeStream.next(() => {}); + changeStream.next(() => { + // ignore + }); } - if (delay != null) { + if (typeof delay === 'number') { setTimeout(triggerError, delay); return; } @@ -88,13 +98,8 @@ function triggerResumableError(changeStream, delay, onClose) { triggerError(); } -/** - * Waits for a change stream to start - * - * @param {ChangeStream} changeStream - * @param {Function} callback - */ -function waitForStarted(changeStream, callback) { +/** Waits for a change stream to start */ +function waitForStarted(changeStream: ChangeStream, callback: () => void) { changeStream.cursor.once('init', () => { callback(); }); @@ -104,9 +109,6 @@ function waitForStarted(changeStream, callback) { * Iterates the next discrete batch of a change stream non-eagerly. This * will return `null` if the next bach is empty, rather than waiting forever * for a non-empty batch. - * - * @param {ChangeStream} changeStream - * @param {Function} callback */ function tryNext(changeStream, callback) { let complete = false; @@ -139,10 +141,6 @@ function tryNext(changeStream, callback) { /** * Exhausts a change stream aggregating all responses until the first * empty batch into a returned array of events. - * - * @param {ChangeStream} changeStream - * @param {Function|Array} bag - * @param {Function} [callback] */ function exhaust(changeStream, bag, callback) { if (typeof bag === 'function') { @@ -388,6 +386,7 @@ describe('Change Streams', function () { assert.equal(change.ns.coll, 'docsCallback'); assert.ok(!change.documentKey); assert.equal( + // @ts-expect-error: NODE-4059 will improve typescript capability of change stream change.comment, 'The documentKey field has been projected out of this document.' ); @@ -441,7 +440,9 @@ describe('Change Streams', function () { this.defer(() => changeStream3.close()); setTimeout(() => { - this.defer(collection1.insert({ a: 1 }).then(() => collection2.insert({ a: 1 }))); + this.defer( + collection1.insertMany([{ a: 1 }]).then(() => collection2.insertMany([{ a: 1 }])) + ); }, 50); Promise.resolve() @@ -474,8 +475,11 @@ describe('Change Streams', function () { assert.equal(changes[1].ns.coll, 'simultaneous2'); assert.equal(changes[2].ns.coll, 'simultaneous2'); + // @ts-expect-error: NODE-4059 will improve typescript capability of change stream assert.equal(changes[0].changeStreamNumber, 1); + // @ts-expect-error: NODE-4059 will improve typescript capability of change stream assert.equal(changes[1].changeStreamNumber, 2); + // @ts-expect-error: NODE-4059 will improve typescript capability of change stream assert.equal(changes[2].changeStreamNumber, 3); }) .then( @@ -568,7 +572,7 @@ describe('Change Streams', function () { // Trigger the first database event waitForStarted(changeStream, () => { - this.defer(database.collection('cacheResumeTokenCallback').insert({ b: 2 })); + this.defer(database.collection('cacheResumeTokenCallback').insertMany([{ b: 2 }])); }); // Fetch the change notification @@ -602,7 +606,7 @@ describe('Change Streams', function () { // trigger the first database event waitForStarted(changeStream, () => { - this.defer(database.collection('cacheResumeTokenPromise').insert({ b: 2 })); + this.defer(database.collection('cacheResumeTokenPromise').insertMany([{ b: 2 }])); }); return changeStream @@ -636,7 +640,7 @@ describe('Change Streams', function () { const collector = new EventCollector(changeStream, ['change']); waitForStarted(changeStream, () => { // Trigger the first database event - db.collection('cacheResumeTokenListener').insert({ b: 2 }, (err, result) => { + db.collection('cacheResumeTokenListener').insertMany([{ b: 2 }], (err, result) => { expect(err).to.not.exist; expect(result).property('insertedCount').to.equal(1); @@ -665,7 +669,9 @@ describe('Change Streams', function () { const collection = database.collection('resumetokenProjectedOutCallback'); const changeStream = collection.watch([{ $project: { _id: false } }]); - changeStream.hasNext(() => {}); // trigger initialize + changeStream.hasNext(() => { + // trigger initialize + }); changeStream.cursor.on('init', () => { collection.insertOne({ b: 2 }, (err, res) => { @@ -704,7 +710,7 @@ describe('Change Streams', function () { const collector = new EventCollector(changeStream, ['change', 'error']); waitForStarted(changeStream, () => { - collection.insert({ b: 2 }, (err, result) => { + collection.insertMany([{ b: 2 }], (err, result) => { expect(err).to.not.exist; expect(result).property('insertedCount').to.equal(1); @@ -743,6 +749,7 @@ describe('Change Streams', function () { assert.equal(change.ns.coll, 'invalidateListeners'); assert.ok(!change.documentKey); assert.equal( + // @ts-expect-error: NODE-4059 will improve typescript capability of change stream change.comment, 'The documentKey field has been projected out of this document.' ); @@ -765,7 +772,7 @@ describe('Change Streams', function () { // Trigger the first database event waitForStarted(changeStream, () => { - this.defer(database.collection('invalidateListeners').insert({ a: 1 })); + this.defer(database.collection('invalidateListeners').insertMany([{ a: 1 }])); }); }); } @@ -788,7 +795,7 @@ describe('Change Streams', function () { // Trigger the first database event waitForStarted(changeStream, () => { - this.defer(database.collection('invalidateCallback').insert({ a: 1 })); + this.defer(database.collection('invalidateCallback').insertMany([{ a: 1 }])); }); changeStream.next((err, change) => { @@ -856,7 +863,9 @@ describe('Change Streams', function () { // Trigger the first database event waitForStarted(changeStream, () => { - this.defer(database.collection('invalidateCollectionDropPromises').insert({ a: 1 })); + this.defer( + database.collection('invalidateCollectionDropPromises').insertMany([{ a: 1 }]) + ); }); return changeStream @@ -889,19 +898,18 @@ describe('Change Streams', function () { const database = client.db('integration_tests'); const collection = database.collection('resumeAfterTest2'); - let firstChangeStream, secondChangeStream; - let resumeToken; const docs = [{ a: 0 }, { a: 1 }, { a: 2 }]; - firstChangeStream = collection.watch(pipeline); + let secondChangeStream; + const firstChangeStream = collection.watch(pipeline); this.defer(() => firstChangeStream.close()); // Trigger the first database event waitForStarted(firstChangeStream, () => { this.defer( collection - .insert(docs[0]) + .insertMany([docs[0]]) .then(() => collection.insertOne(docs[1])) .then(() => collection.insertOne(docs[2])) ); @@ -978,7 +986,7 @@ describe('Change Streams', function () { this.defer(() => changeStream.close()); waitForStarted(changeStream, () => { - this.defer(collection.insert({ f: 128 })); + this.defer(collection.insertOne({ f: 128 })); }); return changeStream @@ -994,10 +1002,11 @@ describe('Change Streams', function () { assert.equal(change.ns.coll, collection.collectionName); assert.ok(!change.documentKey); assert.equal( + // @ts-expect-error: NODE-4059 will improve typescript capability of change stream change.comment, 'The documentKey field has been projected out of this document.' ); - return collection.update({ f: 128 }, { $set: { c: 2 } }); + return collection.updateOne({ f: 128 }, { $set: { c: 2 } }); }) .then(function () { return changeStream.next(); @@ -1033,7 +1042,9 @@ describe('Change Streams', function () { // Trigger the first database event waitForStarted(changeStream, () => { - this.defer(collection.insert({ i: 128 }).then(() => collection.deleteOne({ i: 128 }))); + this.defer( + collection.insertMany([{ i: 128 }]).then(() => collection.deleteOne({ i: 128 })) + ); }); return changeStream @@ -1049,12 +1060,13 @@ describe('Change Streams', function () { assert.equal(change.ns.coll, collection.collectionName); assert.ok(!change.documentKey); assert.equal( + // @ts-expect-error: NODE-4059 will improve typescript capability of change stream change.comment, 'The documentKey field has been projected out of this document.' ); // Trigger the second database event - return collection.update({ i: 128 }, { $set: { c: 2 } }); + return collection.updateOne({ i: 128 }, { $set: { c: 2 } }); }) .then(() => changeStream.hasNext()) .then(function (hasNext) { @@ -1063,6 +1075,7 @@ describe('Change Streams', function () { }) .then(function (change) { assert.equal(change.operationType, 'delete'); + // @ts-expect-error: NODE-4059 will improve typescript capability of change stream assert.equal(change.lookedUpDocument, null); }); }); @@ -1120,7 +1133,6 @@ describe('Change Streams', function () { test: function (done) { const configuration = this.configuration; - const stream = require('stream'); const client = configuration.newClient(); client.connect((err, client) => { @@ -1132,9 +1144,10 @@ describe('Change Streams', function () { const changeStream = collection.watch(pipeline); this.defer(() => changeStream.close()); - const outStream = new stream.PassThrough({ objectMode: true }); + const outStream = new PassThrough({ objectMode: true }); // Make a stream transforming to JSON and piping to the file + // @ts-expect-error: NODE-XXXX This is a type bug!! changeStream.stream({ transform: JSON.stringify }).pipe(outStream); outStream @@ -1150,7 +1163,7 @@ describe('Change Streams', function () { .on('error', done); waitForStarted(changeStream, () => { - this.defer(collection.insert({ a: 1 })); + this.defer(collection.insertMany([{ a: 1 }])); }); }); } @@ -1206,7 +1219,7 @@ describe('Change Streams', function () { }); waitForStarted(changeStream, () => { - this.defer(collection.insert({ a: 1407 })); + this.defer(collection.insertMany([{ a: 1407 }])); }); }); } @@ -1219,9 +1232,9 @@ describe('Change Streams', function () { const client = configuration.newClient(); const collectionName = 'resumeAfterKillCursor'; - const changeStreamOptions = { + const changeStreamOptions: ChangeStreamOptions = { fullDocument: 'updateLookup', - collation: { maxVariable: 'punct' }, + collation: { locale: 'en', maxVariable: 'punct' }, maxAwaitTimeMS: 20000, batchSize: 200 }; @@ -1249,7 +1262,7 @@ describe('Change Streams', function () { coll = client.db(this.configuration.db).collection('tester'); changeStream = coll.watch(); kMode = getSymbolFrom(changeStream, 'mode'); - initPromise = new Promise(resolve => waitForStarted(changeStream, resolve)); + initPromise = new Promise(resolve => waitForStarted(changeStream, resolve)); }); afterEach(async function () { @@ -1283,9 +1296,10 @@ describe('Change Streams', function () { metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } }, async test() { expect(changeStream).to.have.property(kMode, false); - // ChangeStream detects emitter usage via 'newListener' event - // so this covers all emitter methods - changeStream.on('change', () => {}); + changeStream.on('change', () => { + // ChangeStream detects emitter usage via 'newListener' event + // so this covers all emitter methods + }); expect(changeStream).to.have.property(kMode, 'emitter'); const errRegex = /ChangeStream cannot be used as an iterator/; @@ -1311,11 +1325,12 @@ describe('Change Streams', function () { expect(res).to.not.exist; expect(changeStream).to.have.property(kMode, 'iterator'); - // This does throw synchronously - // the newListener event is called sync - // which calls streamEvents, which calls setIsEmitter, which will throw expect(() => { - changeStream.on('change', () => {}); + changeStream.on('change', () => { + // This does throw synchronously + // the newListener event is called sync + // which calls streamEvents, which calls setIsEmitter, which will throw + }); }).to.throw(/ChangeStream cannot be used as an EventEmitter/); } }); @@ -1371,18 +1386,16 @@ describe('Change Streams', function () { it('when invoked with promises', { metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } }, test: function () { - const test = this; - - function read() { + const read = () => { return Promise.resolve() .then(() => changeStream.next()) .then(() => changeStream.next()) .then(() => { - test.defer(lastWrite()); + this.defer(lastWrite()); const nextP = changeStream.next(); return changeStream.close().then(() => nextP); }); - } + }; return Promise.all([read(), write()]).then( () => Promise.reject(new Error('Expected operation to fail with error')), @@ -1415,7 +1428,11 @@ describe('Change Streams', function () { }); }); - ops.push(write().catch(() => {})); + ops.push( + write().catch(() => { + // ignore + }) + ); } }); @@ -1447,7 +1464,9 @@ describe('Change Streams', function () { waitForStarted(changeStream, () => write() .then(() => lastWrite()) - .catch(() => {}) + .catch(() => { + // ignore + }) ); } }); @@ -1561,7 +1580,7 @@ describe('Change Streams', function () { coll.insertOne({ x: 2 }, { writeConcern: { w: 'majority', j: true } }, err => { expect(err).to.not.exist; - exhaust(changeStream, (err, bag) => { + exhaust(changeStream, [], (err, bag) => { expect(err).to.not.exist; const finalOperation = bag.pop(); expect(finalOperation).to.containSubset({ @@ -1575,149 +1594,150 @@ describe('Change Streams', function () { } }); }); -}); -describe('Change Stream Resume Error Tests', function () { - it('should continue emitting change events after a resumable error', { - metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } }, - test: withChangeStream((collection, changeStream, done) => { - const docs = []; - changeStream.on('change', change => { - expect(change).to.exist; - docs.push(change); - if (docs.length === 2) { - expect(docs[0]).to.containSubset({ - operationType: 'insert', - fullDocument: { a: 42 } - }); - expect(docs[1]).to.containSubset({ - operationType: 'insert', - fullDocument: { b: 24 } - }); - done(); - } - }); + describe('Change Stream Resume Error Tests', function () { + it('should continue emitting change events after a resumable error', { + metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } }, + test: withChangeStream((collection, changeStream, done) => { + const docs = []; + changeStream.on('change', change => { + expect(change).to.exist; + docs.push(change); + if (docs.length === 2) { + expect(docs[0]).to.containSubset({ + operationType: 'insert', + fullDocument: { a: 42 } + }); + expect(docs[1]).to.containSubset({ + operationType: 'insert', + fullDocument: { b: 24 } + }); + done(); + } + }); - waitForStarted(changeStream, () => { - collection.insertOne({ a: 42 }, err => { - expect(err).to.not.exist; - triggerResumableError(changeStream, 1000, () => { - collection.insertOne({ b: 24 }, err => { - expect(err).to.not.exist; + waitForStarted(changeStream, () => { + collection.insertOne({ a: 42 }, err => { + expect(err).to.not.exist; + triggerResumableError(changeStream, 1000, () => { + collection.insertOne({ b: 24 }, err => { + expect(err).to.not.exist; + }); }); }); }); - }); - }) - }); + }) + }); - it('should continue iterating changes after a resumable error', { - metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } }, - test: withChangeStream((collection, changeStream, done) => { - waitForStarted(changeStream, () => { - collection.insertOne({ a: 42 }, err => { - expect(err).to.not.exist; - triggerResumableError(changeStream, 250, () => { - changeStream.hasNext((err1, hasNext) => { - expect(err1).to.not.exist; - expect(hasNext).to.be.true; - changeStream.next((err, change) => { - expect(err).to.not.exist; - expect(change).to.containSubset({ - operationType: 'insert', - fullDocument: { b: 24 } + it('should continue iterating changes after a resumable error', { + metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } }, + test: withChangeStream((collection, changeStream, done) => { + waitForStarted(changeStream, () => { + collection.insertOne({ a: 42 }, err => { + expect(err).to.not.exist; + triggerResumableError(changeStream, 250, () => { + changeStream.hasNext((err1, hasNext) => { + expect(err1).to.not.exist; + expect(hasNext).to.be.true; + changeStream.next((err, change) => { + expect(err).to.not.exist; + expect(change).to.containSubset({ + operationType: 'insert', + fullDocument: { b: 24 } + }); + done(); }); - done(); }); + collection.insertOne({ b: 24 }); }); - collection.insertOne({ b: 24 }); }); }); - }); - changeStream.hasNext((err, hasNext) => { - expect(err).to.not.exist; - expect(hasNext).to.be.true; - changeStream.next((err, change) => { + changeStream.hasNext((err, hasNext) => { expect(err).to.not.exist; - expect(change).to.containSubset({ - operationType: 'insert', - fullDocument: { a: 42 } + expect(hasNext).to.be.true; + changeStream.next((err, change) => { + expect(err).to.not.exist; + expect(change).to.containSubset({ + operationType: 'insert', + fullDocument: { a: 42 } + }); }); }); - }); - }) - }); + }) + }); - it.skip('should continue piping changes after a resumable error', { - metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } }, - test: withChangeStream((collection, changeStream, done) => { - const d = new PassThrough({ objectMode: true }); - const bucket = []; - d.on('data', data => { - bucket.push(data.fullDocument.x); - if (bucket.length === 2) { - expect(bucket[0]).to.be(1); - expect(bucket[0]).to.be(2); - done(); - } - }); - changeStream.stream().pipe(d); - waitForStarted(changeStream, () => { - collection.insertOne({ x: 1 }, (err, result) => { - expect(err).to.not.exist; - expect(result).to.exist; - triggerResumableError(changeStream, 250, () => { - collection.insertOne({ x: 2 }, (err, result) => { - expect(err).to.not.exist; - expect(result).to.exist; + it.skip('should continue piping changes after a resumable error', { + metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } }, + test: withChangeStream((collection, changeStream, done) => { + const d = new PassThrough({ objectMode: true }); + const bucket = []; + d.on('data', data => { + bucket.push(data.fullDocument.x); + if (bucket.length === 2) { + expect(bucket[0]).to.equal(1); + expect(bucket[1]).to.equal(2); + done(); + } + }); + changeStream.stream().pipe(d); + waitForStarted(changeStream, () => { + collection.insertOne({ x: 1 }, (err, result) => { + expect(err).to.not.exist; + expect(result).to.exist; + triggerResumableError(changeStream, 250, () => { + collection.insertOne({ x: 2 }, (err, result) => { + expect(err).to.not.exist; + expect(result).to.exist; + }); }); }); }); - }); - }) - }).skipReason = 'TODO(NODE-3884): Fix when implementing prose case #3'; -}); -context('NODE-2626 - handle null changes without error', function () { - let mockServer; - afterEach(() => mock.cleanup()); - beforeEach(() => mock.createServer().then(server => (mockServer = server))); - it('changeStream should close if cursor id for initial aggregate is Long.ZERO', function (done) { - mockServer.setMessageHandler(req => { - const doc = req.document; - if (isHello(doc)) { - return req.reply(mock.HELLO); - } - if (doc.aggregate) { - return req.reply({ - ok: 1, - cursor: { - id: Long.ZERO, - firstBatch: [] + }) + }).skipReason = 'TODO(NODE-3884): Fix when implementing prose case #3'; + + describe('NODE-2626 - handle null changes without error', function () { + let mockServer; + afterEach(() => mock.cleanup()); + beforeEach(() => mock.createServer().then(server => (mockServer = server))); + it('changeStream should close if cursor id for initial aggregate is Long.ZERO', function (done) { + mockServer.setMessageHandler(req => { + const doc = req.document; + if (isHello(doc)) { + return req.reply(mock.HELLO); } - }); - } - if (doc.getMore) { - return req.reply({ - ok: 1, - cursor: { - id: new Long(1407, 1407), - nextBatch: [] + if (doc.aggregate) { + return req.reply({ + ok: 1, + cursor: { + id: Long.ZERO, + firstBatch: [] + } + }); } + if (doc.getMore) { + return req.reply({ + ok: 1, + cursor: { + id: new Long(1407, 1407), + nextBatch: [] + } + }); + } + req.reply({ ok: 1 }); + }); + const client = this.configuration.newClient(`mongodb://${mockServer.uri()}/`); + client.connect(err => { + expect(err).to.not.exist; + const collection = client.db('cs').collection('test'); + const changeStream = collection.watch(); + changeStream.next((err, doc) => { + expect(err).to.exist; + expect(doc).to.not.exist; + expect(err.message).to.equal('ChangeStream is closed'); + changeStream.close(() => client.close(done)); + }); }); - } - req.reply({ ok: 1 }); - }); - const client = this.configuration.newClient(`mongodb://${mockServer.uri()}/`); - client.connect(err => { - expect(err).to.not.exist; - const collection = client.db('cs').collection('test'); - const changeStream = collection.watch(); - changeStream.next((err, doc) => { - expect(err).to.exist; - expect(doc).to.not.exist; - expect(err.message).to.equal('ChangeStream is closed'); - changeStream.close(() => client.close(done)); }); }); }); diff --git a/test/integration/change-streams/change_streams.prose.test.js b/test/integration/change-streams/change_streams.prose.test.ts similarity index 89% rename from test/integration/change-streams/change_streams.prose.test.js rename to test/integration/change-streams/change_streams.prose.test.ts index 2db7a343bae..4bb72fcd9c8 100644 --- a/test/integration/change-streams/change_streams.prose.test.js +++ b/test/integration/change-streams/change_streams.prose.test.ts @@ -1,22 +1,36 @@ -const { expect } = require('chai'); -const { LEGACY_HELLO_COMMAND } = require('../../../src/constants'); - -const { setupDatabase } = require('../shared'); -const mock = require('../../tools/mongodb-mock/index'); - -const sinon = require('sinon'); -const { ObjectId, Timestamp, Long, MongoNetworkError } = require('../../../src'); -const { isHello } = require('../../../src/utils'); +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { + ChangeStream, + CommandFailedEvent, + CommandStartedEvent, + CommandSucceededEvent, + Document, + Long, + MongoNetworkError, + ObjectId, + Timestamp +} from '../../../src'; +import { LEGACY_HELLO_COMMAND } from '../../../src/constants'; +import { isHello } from '../../../src/utils'; +import * as mock from '../../tools/mongodb-mock/index'; +import { setupDatabase } from '../shared'; /** * Triggers a fake resumable error on a change stream - * - * @param {ChangeStream} changeStream - * @param {number} [delay] optional delay before triggering error - * @param {Function} onClose callback when cursor closed due this error + * changeStream + * [delay] optional delay before triggering error + * onClose callback when cursor closed due this error */ -function triggerResumableError(changeStream, delay, onClose) { - if (arguments.length === 2) { +function triggerResumableError(changeStream: ChangeStream, onClose?: () => void); +function triggerResumableError(changeStream: ChangeStream, delay: number, onClose?: () => void); +function triggerResumableError( + changeStream: ChangeStream, + delay: number | (() => void), + onClose?: () => void +) { + if (typeof delay === 'function') { onClose = delay; delay = undefined; } @@ -40,10 +54,12 @@ function triggerResumableError(changeStream, delay, onClose) { nextStub.restore(); }); - changeStream.next(() => {}); + changeStream.next(() => { + // ignore + }); } - if (delay != null) { + if (typeof delay === 'number') { setTimeout(triggerError, delay); return; } @@ -51,12 +67,7 @@ function triggerResumableError(changeStream, delay, onClose) { triggerError(); } -/** - * Waits for a change stream to start - * - * @param {ChangeStream} changeStream - * @param {Function} callback - */ +/** Waits for a change stream to start */ function waitForStarted(changeStream, callback) { changeStream.cursor.once('init', () => { callback(); @@ -177,8 +188,10 @@ describe('Change Stream prose tests', function () { let server; let client; - let finish = err => { - finish = () => {}; + let finish = (err?: Error) => { + finish = () => { + // ignore + }; Promise.resolve() .then(() => changeStream && changeStream.close()) .then(() => client && client.close()) @@ -254,6 +267,25 @@ describe('Change Stream prose tests', function () { describe('Change Stream prose 11-14', () => { class MockServerManager { + config: any; + cmdList: Set; + database: string; + collection: string; + ns: string; + _timestampCounter: number; + cursorId: Long; + commandIterators: any; + promise: Promise; + server: any; + client: any; + apm: { + started: CommandStartedEvent[]; + succeeded: CommandSucceededEvent[]; + failed: CommandFailedEvent[]; + }; + changeStream: any; + resumeTokenChangedEvents: any[]; + namespace: any; constructor(config, commandIterators) { this.config = config; this.cmdList = new Set([ @@ -294,11 +326,13 @@ describe('Change Stream prose tests', function () { this.client = this.config.newClient(this.mongodbURI, { monitorCommands: true }); return this.client.connect().then(() => { this.apm = { started: [], succeeded: [], failed: [] }; - [ - ['commandStarted', this.apm.started], - ['commandSucceeded', this.apm.succeeded], - ['commandFailed', this.apm.failed] - ].forEach(opts => { + ( + [ + ['commandStarted', this.apm.started], + ['commandSucceeded', this.apm.succeeded], + ['commandFailed', this.apm.failed] + ] as const + ).forEach(opts => { const eventName = opts[0]; const target = opts[1]; @@ -312,7 +346,7 @@ describe('Change Stream prose tests', function () { }); } - makeChangeStream(options) { + makeChangeStream(options?: Document) { this.changeStream = this.client .db(this.database) .collection(this.collection) @@ -326,7 +360,7 @@ describe('Change Stream prose tests', function () { return this.changeStream; } - teardown(e) { + teardown(e?: Error) { let promise = Promise.resolve(); if (this.changeStream) { promise = promise.then(() => this.changeStream.close()).catch(); @@ -420,7 +454,7 @@ describe('Change Stream prose tests', function () { const batch = Array.from({ length: config.numDocuments || 0 }).map(() => this.changeEvent() ); - const cursor = { + const cursor: Document = { [batchKey]: batch, id: this.cursorId, ns: this.ns @@ -431,7 +465,7 @@ describe('Change Stream prose tests', function () { return cursor; } - changeEvent(operationType, fullDocument) { + changeEvent(operationType?: string, fullDocument?: Document) { fullDocument = fullDocument || {}; return { _id: this.resumeToken(), @@ -482,6 +516,7 @@ describe('Change Stream prose tests', function () { const tokens = manager.resumeTokenChangedEvents.map(e => e.resumeToken); const successes = manager.apm.succeeded.map(e => { try { + // @ts-expect-error: e.reply is unknown return e.reply.cursor; } catch (e) { return {}; @@ -531,6 +566,7 @@ describe('Change Stream prose tests', function () { const tokens = manager.resumeTokenChangedEvents.map(e => e.resumeToken); const successes = manager.apm.succeeded.map(e => { try { + // @ts-expect-error: e.reply is unknown return e.reply.cursor; } catch (e) { return {}; @@ -559,7 +595,7 @@ describe('Change Stream prose tests', function () { return manager .ready() .then(() => { - return new Promise(resolve => { + return new Promise(resolve => { const changeStream = manager.makeChangeStream({ resumeAfter }); let counter = 0; changeStream.cursor.on('response', () => { @@ -570,8 +606,9 @@ describe('Change Stream prose tests', function () { counter += 1; }); - // Note: this is expected to fail - changeStream.next().catch(() => {}); + changeStream.next().catch(() => { + // Note: this is expected to fail + }); }); }) .then( @@ -596,7 +633,7 @@ describe('Change Stream prose tests', function () { return manager .ready() .then(() => { - return new Promise(resolve => { + return new Promise(resolve => { const changeStream = manager.makeChangeStream(); let counter = 0; changeStream.cursor.on('response', () => { @@ -607,8 +644,9 @@ describe('Change Stream prose tests', function () { counter += 1; }); - // Note: this is expected to fail - changeStream.next().catch(() => {}); + changeStream.next().catch(() => { + // Note: this is expected to fail + }); }); }) .then( @@ -632,7 +670,9 @@ describe('Change Stream prose tests', function () { aggregate: (function* () { yield { numDocuments: 2, postBatchResumeToken: true }; })(), - getMore: (function* () {})() + getMore: (function* () { + // fake getMore + })() }); return manager @@ -648,6 +688,7 @@ describe('Change Stream prose tests', function () { const tokens = manager.resumeTokenChangedEvents.map(e => e.resumeToken); const successes = manager.apm.succeeded.map(e => { try { + // @ts-expect-error: e.reply is unknown return e.reply.cursor; } catch (e) { return {}; @@ -691,15 +732,16 @@ describe('Change Stream prose tests', function () { return manager .ready() .then(() => { - return new Promise(resolve => { + return new Promise(resolve => { const changeStream = manager.makeChangeStream({ startAfter, resumeAfter }); changeStream.cursor.once('response', () => { token = changeStream.resumeToken; resolve(); }); - // Note: this is expected to fail - changeStream.next().catch(() => {}); + changeStream.next().catch(() => { + // Note: this is expected to fail + }); }); }) .then( @@ -725,15 +767,16 @@ describe('Change Stream prose tests', function () { return manager .ready() .then(() => { - return new Promise(resolve => { + return new Promise(resolve => { const changeStream = manager.makeChangeStream({ resumeAfter }); changeStream.cursor.once('response', () => { token = changeStream.resumeToken; resolve(); }); - // Note: this is expected to fail - changeStream.next().catch(() => {}); + changeStream.next().catch(() => { + // Note: this is expected to fail + }); }); }) .then( @@ -758,15 +801,16 @@ describe('Change Stream prose tests', function () { return manager .ready() .then(() => { - return new Promise(resolve => { + return new Promise(resolve => { const changeStream = manager.makeChangeStream(); changeStream.cursor.once('response', () => { token = changeStream.resumeToken; resolve(); }); - // Note: this is expected to fail - changeStream.next().catch(() => {}); + changeStream.next().catch(() => { + // Note: this is expected to fail + }); }); }) .then( diff --git a/test/integration/change-streams/change_streams.spec.test.js b/test/integration/change-streams/change_streams.spec.test.ts similarity index 76% rename from test/integration/change-streams/change_streams.spec.test.js rename to test/integration/change-streams/change_streams.spec.test.ts index 436e7f493f5..34edaa75694 100644 --- a/test/integration/change-streams/change_streams.spec.test.js +++ b/test/integration/change-streams/change_streams.spec.test.ts @@ -1,18 +1,17 @@ -'use strict'; +import { expect } from 'chai'; +import * as path from 'path'; -const path = require('path'); -const { expect } = require('chai'); -const { loadSpecTests } = require('../../spec'); -const { runUnifiedSuite } = require('../../tools/unified-spec-runner/runner'); -const camelCase = require('lodash.camelcase'); -const { LEGACY_HELLO_COMMAND } = require('../../../src/constants'); -const { delay, setupDatabase } = require('../shared'); +import { Document, MongoClient } from '../../../src'; +import { LEGACY_HELLO_COMMAND } from '../../../src/constants'; +import { loadSpecTests } from '../../spec'; +import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner'; +import { delay, setupDatabase } from '../shared'; describe('Change Streams Spec - Unified', function () { runUnifiedSuite(loadSpecTests(path.join('change-streams', 'unified'))); }); -// TODO: NODE-3819: Unskip flaky MacOS tests. +// TODO(NODE-3819): Unskip flaky MacOS tests. const maybeDescribe = process.platform === 'darwin' ? describe.skip : describe; maybeDescribe('Change Stream Spec - v1', function () { let globalClient; @@ -32,7 +31,7 @@ maybeDescribe('Change Stream Spec - v1', function () { after(function () { const gc = globalClient; globalClient = undefined; - return new Promise(r => gc.close(() => r())); + return new Promise(r => gc.close(() => r())); }); loadSpecTests(path.join('change-streams', 'legacy')).forEach(suite => { @@ -99,7 +98,7 @@ maybeDescribe('Change Stream Spec - v1', function () { function generateMetadata(test) { const topology = test.topology; - const requires = {}; + const requires: MongoDBMetadataUI['requires'] = {}; const versionLimits = []; if (test.minServerVersion) { versionLimits.push(`>=${test.minServerVersion}`); @@ -245,7 +244,7 @@ maybeDescribe('Change Stream Spec - v1', function () { .reduce((p, op) => p.then(op), delay(200)); } - function makeChangeStreamCloseFn(changeStream) { + function makeChangeStreamCloseFn(changeStream): (error?: any, value?: any) => Promise { return function close(error, value) { return new Promise((resolve, reject) => { changeStream.close(err => { @@ -259,33 +258,44 @@ maybeDescribe('Change Stream Spec - v1', function () { } function normalizeAPMEvent(raw) { - return Object.keys(raw).reduce((agg, key) => { - agg[camelCase(key)] = raw[key]; - return agg; - }, {}); + const rawKeys = Object.keys(raw); + rawKeys.sort(); + expect(rawKeys, 'test runner only supports these keys, is there a new one?').to.deep.equal([ + 'command', + 'command_name', + 'database_name' + ]); + return { + command: raw.command, + commandName: raw.command_name, + databaseName: raw.database_name + }; } - function makeOperation(client, op) { - const target = client.db(op.database).collection(op.collection); - const command = op.name; - const args = []; - if (op.arguments) { - if (op.arguments.document) { - args.push(op.arguments.document); - } - if (op.arguments.filter) { - args.push(op.arguments.filter); - } - if (op.arguments.update) { - args.push(op.arguments.update); - } - if (op.arguments.replacement) { - args.push(op.arguments.replacement); - } - if (op.arguments.to) { - args.push(op.arguments.to); - } + function makeOperation(client: MongoClient, op: Document) { + const collection = client.db(op.database).collection(op.collection); + switch (op.name) { + case 'insertOne': + expect(op.arguments).to.have.property('document').that.is.an('object'); + return () => collection.insertOne(op.arguments.document); + case 'updateOne': + expect(op.arguments).to.have.property('filter').that.is.an('object'); + expect(op.arguments).to.have.property('update').that.is.an('object'); + return () => collection.updateOne(op.arguments.filter, op.arguments.update); + case 'replaceOne': + expect(op.arguments).to.have.property('filter').that.is.an('object'); + expect(op.arguments).to.have.property('replacement').that.is.an('object'); + return () => collection.replaceOne(op.arguments.filter, op.arguments.replacement); + case 'deleteOne': + expect(op.arguments).to.have.property('filter').that.is.an('object'); + return () => collection.deleteOne(op.arguments.filter); + case 'rename': + expect(op.arguments).to.have.property('to').that.is.a('string'); + return () => collection.rename(op.arguments.to); + case 'drop': + return () => collection.drop(); + default: + throw new Error(`runner does not support ${op.name}`); } - return () => target[command].apply(target, args); } }); diff --git a/test/integration/enumerate_collections.test.ts b/test/integration/enumerate_collections.test.ts index 802d876a4f8..e93239be760 100644 --- a/test/integration/enumerate_collections.test.ts +++ b/test/integration/enumerate_collections.test.ts @@ -1,98 +1,95 @@ -import { runUnifiedSuite } from '../tools/unified-spec-runner/runner'; import { TestBuilder, UnifiedTestSuiteBuilder } from '../tools/utils'; -const testSuite = new UnifiedTestSuiteBuilder('listCollections with comment option') - .initialData({ - collectionName: 'coll0', - databaseName: '', - documents: [{ _id: 1, x: 11 }] - }) - .databaseName('listCollections-with-falsy-values') - .test( - new TestBuilder('listCollections should not send comment for server versions < 4.4') - .runOnRequirement({ maxServerVersion: '4.3.99' }) - .operation({ - name: 'listCollections', - arguments: { - filter: {}, - comment: 'string value' - }, - object: 'database0' - }) - .expectEvents({ - client: 'client0', - events: [ - { - commandStartedEvent: { - command: { - listCollections: 1, - comment: { $$exists: false } +describe('listCollections', () => { + UnifiedTestSuiteBuilder.describe('comment option') + .createEntities(UnifiedTestSuiteBuilder.defaultEntities) + .initialData({ + collectionName: 'collection0', + databaseName: 'database0', + documents: [{ _id: 1, x: 11 }] + }) + .test( + new TestBuilder('listCollections should not send comment for server versions < 4.4') + .runOnRequirement({ maxServerVersion: '4.3.99' }) + .operation({ + name: 'listCollections', + arguments: { + filter: {}, + comment: 'string value' + }, + object: 'database0' + }) + .expectEvents({ + client: 'client0', + events: [ + { + commandStartedEvent: { + command: { + listCollections: 1, + comment: { $$exists: false } + } } } - } - ] - }) - .toJSON() - ) - .test( - new TestBuilder('listCollections should send string comment for server versions >= 4.4') - .runOnRequirement({ minServerVersion: '4.4.0' }) - .operation({ - name: 'listCollections', - arguments: { - filter: {}, - comment: 'string value' - }, - object: 'database0' - }) - .expectEvents({ - client: 'client0', - events: [ - { - commandStartedEvent: { - command: { - listCollections: 1, - comment: 'string value' + ] + }) + .toJSON() + ) + .test( + new TestBuilder('listCollections should send string comment for server versions >= 4.4') + .runOnRequirement({ minServerVersion: '4.4.0' }) + .operation({ + name: 'listCollections', + arguments: { + filter: {}, + comment: 'string value' + }, + object: 'database0' + }) + .expectEvents({ + client: 'client0', + events: [ + { + commandStartedEvent: { + command: { + listCollections: 1, + comment: 'string value' + } } } - } - ] - }) - .toJSON() - ) - .test( - new TestBuilder('listCollections should send non-string comment for server versions >= 4.4') - .runOnRequirement({ minServerVersion: '4.4.0' }) - .operation({ - name: 'listCollections', - arguments: { - filter: {}, + ] + }) + .toJSON() + ) + .test( + new TestBuilder('listCollections should send non-string comment for server versions >= 4.4') + .runOnRequirement({ minServerVersion: '4.4.0' }) + .operation({ + name: 'listCollections', + arguments: { + filter: {}, - comment: { - key: 'value' - } - }, - object: 'database0' - }) - .expectEvents({ - client: 'client0', - events: [ - { - commandStartedEvent: { - command: { - listCollections: 1, - comment: { - key: 'value' + comment: { + key: 'value' + } + }, + object: 'database0' + }) + .expectEvents({ + client: 'client0', + events: [ + { + commandStartedEvent: { + command: { + listCollections: 1, + comment: { + key: 'value' + } } } } - } - ] - }) - .toJSON() - ) - .toJSON(); - -describe('listCollections w/ comment option', () => { - runUnifiedSuite([testSuite]); + ] + }) + .toJSON() + ) + .run(); }); diff --git a/test/integration/enumerate_databases.test.ts b/test/integration/enumerate_databases.test.ts index 3da8af66410..686c85dab99 100644 --- a/test/integration/enumerate_databases.test.ts +++ b/test/integration/enumerate_databases.test.ts @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { AddUserOptions, MongoClient, MongoServerError } from '../../src'; -import { runUnifiedSuite } from '../tools/unified-spec-runner/runner'; import { TestBuilder, UnifiedTestSuiteBuilder } from '../tools/utils'; const metadata: MongoDBMetadataUI = { @@ -15,226 +14,224 @@ const metadata: MongoDBMetadataUI = { } }; -describe('listDatabases() authorizedDatabases flag', function () { - const username = 'a'; - const password = 'b'; - const mockAuthorizedDb = 'enumerate_databases'; - const mockAuthorizedCollection = 'enumerate_databases_collection'; +describe('listDatabases()', function () { + describe('authorizedDatabases option', () => { + const username = 'a'; + const password = 'b'; + const mockAuthorizedDb = 'enumerate_databases'; + const mockAuthorizedCollection = 'enumerate_databases_collection'; - let adminClient: MongoClient; - let authorizedClient: MongoClient; + let adminClient: MongoClient; + let authorizedClient: MongoClient; - const authorizedUserOptions: AddUserOptions = { - roles: [{ role: 'read', db: mockAuthorizedDb }] - }; + const authorizedUserOptions: AddUserOptions = { + roles: [{ role: 'read', db: mockAuthorizedDb }] + }; - beforeEach(async function () { - adminClient = this.configuration.newClient(); - await adminClient.connect(); + beforeEach(async function () { + adminClient = this.configuration.newClient(); + await adminClient.connect(); - await adminClient - .db(mockAuthorizedDb) - .createCollection(mockAuthorizedCollection) - .catch(() => null); + await adminClient + .db(mockAuthorizedDb) + .createCollection(mockAuthorizedCollection) + .catch(() => null); - await adminClient.db('admin').addUser(username, password, authorizedUserOptions); + await adminClient.db('admin').addUser(username, password, authorizedUserOptions); - authorizedClient = this.configuration.newClient({ - auth: { username: username, password: password } + authorizedClient = this.configuration.newClient({ + auth: { username: username, password: password } + }); + await authorizedClient.connect(); }); - await authorizedClient.connect(); - }); - afterEach(async function () { - await adminClient?.db('admin').removeUser(username); - await adminClient?.db(mockAuthorizedDb).dropDatabase(); - await adminClient?.close(); - await authorizedClient?.close(); - }); + afterEach(async function () { + await adminClient?.db('admin').removeUser(username); + await adminClient?.db(mockAuthorizedDb).dropDatabase(); + await adminClient?.close(); + await authorizedClient?.close(); + }); - it( - 'should list all databases when admin client sets authorizedDatabases to true', - metadata, - async function () { - const adminListDbs = await adminClient - .db() - .admin() - .listDatabases({ authorizedDatabases: true }); - const adminDbs = adminListDbs.databases.map(({ name }) => name); - - // no change in the dbs listed since we're using the admin user - expect(adminDbs).to.have.length.greaterThan(1); - expect(adminDbs.filter(db => db === mockAuthorizedDb)).to.have.lengthOf(1); - expect(adminDbs.filter(db => db !== mockAuthorizedDb)).to.have.length.greaterThan(1); - } - ); - - it( - 'should list all databases when admin client sets authorizedDatabases to false', - metadata, - async function () { - const adminListDbs = await adminClient - .db() - .admin() - .listDatabases({ authorizedDatabases: false }); - const adminDbs = adminListDbs.databases.map(({ name }) => name); - - // no change in the dbs listed since we're using the admin user - expect(adminDbs).to.have.length.greaterThan(1); - expect(adminDbs.filter(db => db === mockAuthorizedDb)).to.have.lengthOf(1); - expect(adminDbs.filter(db => db !== mockAuthorizedDb)).to.have.length.greaterThan(1); - } - ); - - it( - 'should list authorized databases with authorizedDatabases set to true', - metadata, - async function () { - const adminListDbs = await adminClient.db().admin().listDatabases(); - const authorizedListDbs = await authorizedClient - .db() - .admin() - .listDatabases({ authorizedDatabases: true }); - const adminDbs = adminListDbs.databases; - const authorizedDbs = authorizedListDbs.databases; - - expect(adminDbs).to.have.length.greaterThan(1); - expect(authorizedDbs).to.have.lengthOf(1); - - expect(adminDbs.filter(db => db.name === mockAuthorizedDb)).to.have.lengthOf(1); - expect(adminDbs.filter(db => db.name !== mockAuthorizedDb)).to.have.length.greaterThan(1); - expect(authorizedDbs.filter(db => db.name === mockAuthorizedDb)).to.have.lengthOf(1); - } - ); - - it( - 'should list authorized databases by default with authorizedDatabases unspecified', - metadata, - async function () { - const adminListDbs = await adminClient.db().admin().listDatabases(); - const authorizedListDbs = await authorizedClient.db().admin().listDatabases(); - const adminDbs = adminListDbs.databases; - const authorizedDbs = authorizedListDbs.databases; - - expect(adminDbs).to.have.length.greaterThan(1); - expect(authorizedDbs).to.have.lengthOf(1); - - expect(adminDbs.filter(db => db.name === mockAuthorizedDb)).to.have.lengthOf(1); - expect(adminDbs.filter(db => db.name !== mockAuthorizedDb)).to.have.length.greaterThan(1); - expect(authorizedDbs.filter(db => db.name === mockAuthorizedDb)).to.have.lengthOf(1); - } - ); - - it( - 'should not show authorized databases with authorizedDatabases set to false', - metadata, - async function () { - let thrownError; - try { - await authorizedClient.db().admin().listDatabases({ authorizedDatabases: false }); - } catch (error) { - thrownError = error; + it( + 'should list all databases when admin client sets authorizedDatabases to true', + metadata, + async function () { + const adminListDbs = await adminClient + .db() + .admin() + .listDatabases({ authorizedDatabases: true }); + const adminDbs = adminListDbs.databases.map(({ name }) => name); + + // no change in the dbs listed since we're using the admin user + expect(adminDbs).to.have.length.greaterThan(1); + expect(adminDbs.filter(db => db === mockAuthorizedDb)).to.have.lengthOf(1); + expect(adminDbs.filter(db => db !== mockAuthorizedDb)).to.have.length.greaterThan(1); } + ); + + it( + 'should list all databases when admin client sets authorizedDatabases to false', + metadata, + async function () { + const adminListDbs = await adminClient + .db() + .admin() + .listDatabases({ authorizedDatabases: false }); + const adminDbs = adminListDbs.databases.map(({ name }) => name); + + // no change in the dbs listed since we're using the admin user + expect(adminDbs).to.have.length.greaterThan(1); + expect(adminDbs.filter(db => db === mockAuthorizedDb)).to.have.lengthOf(1); + expect(adminDbs.filter(db => db !== mockAuthorizedDb)).to.have.length.greaterThan(1); + } + ); + + it( + 'should list authorized databases with authorizedDatabases set to true', + metadata, + async function () { + const adminListDbs = await adminClient.db().admin().listDatabases(); + const authorizedListDbs = await authorizedClient + .db() + .admin() + .listDatabases({ authorizedDatabases: true }); + const adminDbs = adminListDbs.databases; + const authorizedDbs = authorizedListDbs.databases; + + expect(adminDbs).to.have.length.greaterThan(1); + expect(authorizedDbs).to.have.lengthOf(1); + + expect(adminDbs.filter(db => db.name === mockAuthorizedDb)).to.have.lengthOf(1); + expect(adminDbs.filter(db => db.name !== mockAuthorizedDb)).to.have.length.greaterThan(1); + expect(authorizedDbs.filter(db => db.name === mockAuthorizedDb)).to.have.lengthOf(1); + } + ); + + it( + 'should list authorized databases by default with authorizedDatabases unspecified', + metadata, + async function () { + const adminListDbs = await adminClient.db().admin().listDatabases(); + const authorizedListDbs = await authorizedClient.db().admin().listDatabases(); + const adminDbs = adminListDbs.databases; + const authorizedDbs = authorizedListDbs.databases; + + expect(adminDbs).to.have.length.greaterThan(1); + expect(authorizedDbs).to.have.lengthOf(1); + + expect(adminDbs.filter(db => db.name === mockAuthorizedDb)).to.have.lengthOf(1); + expect(adminDbs.filter(db => db.name !== mockAuthorizedDb)).to.have.length.greaterThan(1); + expect(authorizedDbs.filter(db => db.name === mockAuthorizedDb)).to.have.lengthOf(1); + } + ); + + it( + 'should not show authorized databases with authorizedDatabases set to false', + metadata, + async function () { + let thrownError; + try { + await authorizedClient.db().admin().listDatabases({ authorizedDatabases: false }); + } catch (error) { + thrownError = error; + } + + // check correctly produces an 'Insufficient permissions to list all databases' error + expect(thrownError).to.be.instanceOf(MongoServerError); + expect(thrownError).to.have.property('message').that.includes('list'); + } + ); + }); - // check correctly produces an 'Insufficient permissions to list all databases' error - expect(thrownError).to.be.instanceOf(MongoServerError); - expect(thrownError).to.have.property('message').that.includes('list'); - } - ); -}); - -const testSuite = new UnifiedTestSuiteBuilder('listDatabases with comment option') - .initialData({ - collectionName: 'coll0', - databaseName: '', - documents: [{ _id: 1, x: 11 }] - }) - .databaseName('listDatabases-with-falsy-values') - .test( - new TestBuilder('listDatabases should not send comment for server versions < 4.4') - .runOnRequirement({ maxServerVersion: '4.3.99' }) - .operation({ - name: 'listDatabases', - arguments: { - filter: {}, - comment: 'string value' - }, - object: 'client0' - }) - .expectEvents({ - client: 'client0', - events: [ - { - commandStartedEvent: { - command: { - listDatabases: 1, - comment: { $$exists: false } + UnifiedTestSuiteBuilder.describe('comment option') + .createEntities(UnifiedTestSuiteBuilder.defaultEntities) + .initialData({ + collectionName: 'collection0', + databaseName: 'database0', + documents: [{ _id: 1, x: 11 }] + }) + .test( + new TestBuilder('listDatabases should not send comment for server versions < 4.4') + .runOnRequirement({ maxServerVersion: '4.3.99' }) + .operation({ + name: 'listDatabases', + arguments: { + filter: {}, + comment: 'string value' + }, + object: 'client0' + }) + .expectEvents({ + client: 'client0', + events: [ + { + commandStartedEvent: { + command: { + listDatabases: 1, + comment: { $$exists: false } + } } } - } - ] - }) - .toJSON() - ) - .test( - new TestBuilder('listDatabases should send string comment for server versions >= 4.4') - .runOnRequirement({ minServerVersion: '4.4.0' }) - .operation({ - name: 'listDatabases', - arguments: { - filter: {}, - comment: 'string value' - }, - object: 'client0' - }) - .expectEvents({ - client: 'client0', - events: [ - { - commandStartedEvent: { - command: { - listDatabases: 1, - comment: 'string value' + ] + }) + .toJSON() + ) + .test( + new TestBuilder('listDatabases should send string comment for server versions >= 4.4') + .runOnRequirement({ minServerVersion: '4.4.0' }) + .operation({ + name: 'listDatabases', + arguments: { + filter: {}, + comment: 'string value' + }, + object: 'client0' + }) + .expectEvents({ + client: 'client0', + events: [ + { + commandStartedEvent: { + command: { + listDatabases: 1, + comment: 'string value' + } } } - } - ] - }) - .toJSON() - ) - .test( - new TestBuilder('listDatabases should send non-string comment for server versions >= 4.4') - .runOnRequirement({ minServerVersion: '4.4.0' }) - .operation({ - name: 'listDatabases', - arguments: { - filter: {}, - - comment: { - key: 'value' - } - }, - object: 'client0' - }) - .expectEvents({ - client: 'client0', - events: [ - { - commandStartedEvent: { - command: { - listDatabases: 1, - comment: { - key: 'value' + ] + }) + .toJSON() + ) + .test( + new TestBuilder('listDatabases should send non-string comment for server versions >= 4.4') + .runOnRequirement({ minServerVersion: '4.4.0' }) + .operation({ + name: 'listDatabases', + arguments: { + filter: {}, + + comment: { + key: 'value' + } + }, + object: 'client0' + }) + .expectEvents({ + client: 'client0', + events: [ + { + commandStartedEvent: { + command: { + listDatabases: 1, + comment: { + key: 'value' + } } } } - } - ] - }) - .toJSON() - ) - .toJSON(); - -describe('listDatabases w/ comment option', () => { - runUnifiedSuite([testSuite]); + ] + }) + .toJSON() + ) + .run(); }); diff --git a/test/integration/enumerate_indexes.test.ts b/test/integration/enumerate_indexes.test.ts index 4ca9cd9455f..4a6264d6a48 100644 --- a/test/integration/enumerate_indexes.test.ts +++ b/test/integration/enumerate_indexes.test.ts @@ -1,95 +1,92 @@ -import { runUnifiedSuite } from '../tools/unified-spec-runner/runner'; import { TestBuilder, UnifiedTestSuiteBuilder } from '../tools/utils'; -const testSuite = new UnifiedTestSuiteBuilder('listIndexes with comment option') - .initialData({ - collectionName: 'coll0', - databaseName: '', - documents: [{ _id: 1, x: 11 }] - }) - .databaseName('listIndexes-with-falsy-values') - .test( - new TestBuilder('listIndexes should not send comment for server versions < 4.4') - .runOnRequirement({ maxServerVersion: '4.3.99' }) - .operation({ - name: 'listIndexes', - arguments: { - filter: {}, - comment: 'string value' - }, - object: 'collection0' - }) - .expectEvents({ - client: 'client0', - events: [ - { - commandStartedEvent: { - command: { - listIndexes: 'coll0', - comment: { $$exists: false } +describe('listIndexes()', () => { + UnifiedTestSuiteBuilder.describe('comment option') + .createEntities(UnifiedTestSuiteBuilder.defaultEntities) + .initialData({ + collectionName: 'collection0', + databaseName: 'database0', + documents: [{ _id: 1, x: 11 }] + }) + .test( + new TestBuilder('listIndexes should not send comment for server versions < 4.4') + .runOnRequirement({ maxServerVersion: '4.3.99' }) + .operation({ + name: 'listIndexes', + arguments: { + filter: {}, + comment: 'string value' + }, + object: 'collection0' + }) + .expectEvents({ + client: 'client0', + events: [ + { + commandStartedEvent: { + command: { + listIndexes: 'collection0', + comment: { $$exists: false } + } } } - } - ] - }) - .toJSON() - ) - .test( - new TestBuilder('listIndexes should send string comment for server versions >= 4.4') - .runOnRequirement({ minServerVersion: '4.4.0' }) - .operation({ - name: 'listIndexes', - arguments: { - filter: {}, - comment: 'string value' - }, - object: 'collection0' - }) - .expectEvents({ - client: 'client0', - events: [ - { - commandStartedEvent: { - command: { - comment: 'string value' + ] + }) + .toJSON() + ) + .test( + new TestBuilder('listIndexes should send string comment for server versions >= 4.4') + .runOnRequirement({ minServerVersion: '4.4.0' }) + .operation({ + name: 'listIndexes', + arguments: { + filter: {}, + comment: 'string value' + }, + object: 'collection0' + }) + .expectEvents({ + client: 'client0', + events: [ + { + commandStartedEvent: { + command: { + comment: 'string value' + } } } - } - ] - }) - .toJSON() - ) - .test( - new TestBuilder('listIndexes should send non-string comment for server versions >= 4.4') - .runOnRequirement({ minServerVersion: '4.4.0' }) - .operation({ - name: 'listIndexes', - arguments: { - filter: {}, - comment: { - key: 'value' - } - }, - object: 'collection0' - }) - .expectEvents({ - client: 'client0', - events: [ - { - commandStartedEvent: { - command: { - comment: { - key: 'value' + ] + }) + .toJSON() + ) + .test( + new TestBuilder('listIndexes should send non-string comment for server versions >= 4.4') + .runOnRequirement({ minServerVersion: '4.4.0' }) + .operation({ + name: 'listIndexes', + arguments: { + filter: {}, + comment: { + key: 'value' + } + }, + object: 'collection0' + }) + .expectEvents({ + client: 'client0', + events: [ + { + commandStartedEvent: { + command: { + comment: { + key: 'value' + } } } } - } - ] - }) - .toJSON() - ) - .toJSON(); - -describe('listIndexes w/ comment option', () => { - runUnifiedSuite([testSuite]); + ] + }) + .toJSON() + ) + .run(); }); diff --git a/test/integration/node-specific/comment_with_falsy_values.test.ts b/test/integration/node-specific/comment_with_falsy_values.test.ts index 016430457de..13ae192b25c 100644 --- a/test/integration/node-specific/comment_with_falsy_values.test.ts +++ b/test/integration/node-specific/comment_with_falsy_values.test.ts @@ -1,5 +1,4 @@ import { Long } from '../../../src'; -import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner'; import { TestBuilder, UnifiedTestSuiteBuilder } from '../../tools/utils'; const falsyValues = [0, false, '', Long.ZERO, null, NaN] as const; @@ -58,20 +57,6 @@ const tests = Array.from(generateTestCombinations()).map(({ name, args }) => { .toJSON(); }); -const testSuite = new UnifiedTestSuiteBuilder('Comment with Falsy Values') - .runOnRequirement({ minServerVersion: '4.4.0' }) - .initialData({ - collectionName: 'coll0', - databaseName: '', - documents: [ - { _id: 1, x: 11 }, - { _id: 2, toBeDeleted: true } // This should only be used by the delete test - ] - }) - .databaseName('comment-with-falsy-values') - .test(tests) - .toJSON(); - const testsForChangeStreamsAggregate = falsyValues.map(falsyValue => { const description = `ChangeStreams should pass falsy value ${falsyToString( falsyValue @@ -157,22 +142,34 @@ const testsForGetMore = falsyValues.map(falsyValue => { .toJSON(); }); -const changeStreamTestSuite = new UnifiedTestSuiteBuilder( - 'Change Streams Comment with Falsy Values' -) - .schemaVersion('1.0') - .initialData({ - collectionName: 'coll0', - databaseName: '', - documents: [] - }) - .databaseName('change-streams-comment-with-falsy-values') - .runOnRequirement({ minServerVersion: '4.4.0', topologies: ['replicaset', 'sharded-replicaset'] }) - .test(testsForChangeStreamsAggregate) - .test(testsForGetMore) - .toJSON(); +describe('Comment with falsy values', () => { + UnifiedTestSuiteBuilder.describe('Comment with Falsy Values') + .runOnRequirement({ minServerVersion: '4.4.0' }) + .createEntities(UnifiedTestSuiteBuilder.defaultEntities) + .initialData({ + collectionName: 'collection0', + databaseName: 'database0', + documents: [ + { _id: 1, x: 11 }, + { _id: 2, toBeDeleted: true } // This should only be used by the delete test + ] + }) + .test(tests) + .run(); -describe('comment w/ falsy values ', () => { - runUnifiedSuite([testSuite]); - runUnifiedSuite([changeStreamTestSuite]); + UnifiedTestSuiteBuilder.describe('Change Streams Comment with Falsy Values') + .schemaVersion('1.0') + .createEntities(UnifiedTestSuiteBuilder.defaultEntities) + .initialData({ + collectionName: 'collection0', + databaseName: 'database0', + documents: [] + }) + .runOnRequirement({ + minServerVersion: '4.4.0', + topologies: ['replicaset', 'sharded-replicaset'] + }) + .test(testsForChangeStreamsAggregate) + .test(testsForGetMore) + .run(); }); diff --git a/test/integration/shared.js b/test/integration/shared.js index 4712f3977fe..78d0ccaa6a3 100644 --- a/test/integration/shared.js +++ b/test/integration/shared.js @@ -192,8 +192,8 @@ function withMonitoredClient(commands, options, callback) { /** * Safely perform a test with an arbitrary cursor. * - * @param {Function} cursor any cursor that needs to be closed - * @param {(cursor: object, done: Function) => void} body test body + * @param {{close: () => void}} cursor any cursor that needs to be closed + * @param {(cursor: object, done: Mocha.Done) => void} body test body * @param {Function} done called after cleanup */ function withCursor(cursor, body, done) { diff --git a/test/tools/unified-spec-runner/match.ts b/test/tools/unified-spec-runner/match.ts index 21ae7047bd7..41e2ccfbda0 100644 --- a/test/tools/unified-spec-runner/match.ts +++ b/test/tools/unified-spec-runner/match.ts @@ -1,7 +1,15 @@ import { expect } from 'chai'; import { inspect } from 'util'; -import { Binary, Document, Long, MongoError, ObjectId } from '../../../src'; +import { + Binary, + BSONTypeAlias, + Document, + Long, + MongoError, + ObjectId, + OneOrMore +} from '../../../src'; import { CommandFailedEvent, CommandStartedEvent, @@ -30,7 +38,7 @@ export function isExistsOperator(value: unknown): value is ExistsOperator { return typeof value === 'object' && value != null && '$$exists' in value; } export interface TypeOperator { - $$type: boolean; + $$type: OneOrMore; } export function isTypeOperator(value: unknown): value is TypeOperator { return typeof value === 'object' && value != null && '$$type' in value; diff --git a/test/tools/unified-spec-runner/schema.ts b/test/tools/unified-spec-runner/schema.ts index aedfc24fd82..112f7084957 100644 --- a/test/tools/unified-spec-runner/schema.ts +++ b/test/tools/unified-spec-runner/schema.ts @@ -7,10 +7,63 @@ import type { W } from '../../../src/write_concern'; export const SupportedVersion = '^1.0'; +export const OperationNames = [ + 'abortTransaction', + 'aggregate', + 'assertCollectionExists', + 'assertCollectionNotExists', + 'assertIndexExists', + 'assertIndexNotExists', + 'assertDifferentLsidOnLastTwoCommands', + 'assertSameLsidOnLastTwoCommands', + 'assertSessionDirty', + 'assertSessionNotDirty', + 'assertSessionPinned', + 'assertSessionUnpinned', + 'assertSessionTransactionState', + 'assertNumberConnectionsCheckedOut', + 'bulkWrite', + 'close', + 'commitTransaction', + 'createChangeStream', + 'createCollection', + 'createFindCursor', + 'createIndex', + 'deleteOne', + 'dropCollection', + 'endSession', + 'find', + 'findOneAndReplace', + 'findOneAndUpdate', + 'findOneAndDelete', + 'failPoint', + 'insertOne', + 'insertMany', + 'iterateUntilDocumentOrError', + 'listCollections', + 'listDatabases', + 'listIndexes', + 'replaceOne', + 'startTransaction', + 'targetedFailPoint', + 'delete', + 'download', + 'upload', + 'withTransaction', + 'countDocuments', + 'deleteMany', + 'distinct', + 'estimatedDocumentCount', + 'runCommand', + 'updateMany', + 'updateOne' +] as const; +export type OperationName = typeof OperationNames[number]; + export interface OperationDescription { - name: string; + name: OperationName; object: string; - arguments: Document; + arguments?: Document; expectError?: ExpectedError; expectResult?: unknown; saveResultAsEntity?: string; diff --git a/test/tools/utils.ts b/test/tools/utils.ts index 3d4f4957094..3d513d91091 100644 --- a/test/tools/utils.ts +++ b/test/tools/utils.ts @@ -4,6 +4,7 @@ import { inspect, promisify } from 'util'; import { Logger } from '../../src/logger'; import { deprecateOptions, DeprecateOptionsConfig } from '../../src/utils'; +import { runUnifiedSuite } from './unified-spec-runner/runner'; import { CollectionData, EntityDescription, @@ -72,7 +73,7 @@ export class EventCollector { constructor( obj: { on: (arg0: any, arg1: (event: any) => number) => void }, events: any[], - options: { timeout: number } + options?: { timeout: number } ) { this._events = Object.create(null); this._timeout = options ? options.timeout : 5000; @@ -83,7 +84,17 @@ export class EventCollector { }); } - waitForEvent(eventName: any, count: number, callback: any) { + waitForEvent(eventName: string, callback: (error?: Error, events?: any[]) => void): void; + waitForEvent( + eventName: string, + count: number, + callback: (error?: Error, events?: any[]) => void + ): void; + waitForEvent( + eventName: string, + count: number | ((error?: Error, events?: any[]) => void), + callback?: (error?: Error, events?: any[]) => void + ): void { if (typeof count === 'function') { callback = count; count = 1; @@ -295,6 +306,7 @@ export class TestBuilder { operation(operation: OperationDescription): this { this._operations.push({ object: 'collection0', + arguments: {}, ...operation }); return this; @@ -329,37 +341,51 @@ export class TestBuilder { export class UnifiedTestSuiteBuilder { private _description = 'Default Description'; - private _databaseName = ''; private _schemaVersion = '1.0'; - private _createEntities: EntityDescription[] = [ - { - client: { - id: 'client0', - useMultipleMongoses: true, - observeEvents: ['commandStartedEvent'] - } - }, - { - database: { - id: 'database0', - client: 'client0', - databaseName: '' - } - }, - { - collection: { - id: 'collection0', - database: 'database0', - collectionName: 'coll0' - } - } - ]; + private _createEntities: EntityDescription[]; private _runOnRequirement: RunOnRequirement[] = []; private _initialData: CollectionData[] = []; private _tests: Test[] = []; + static describe(title: string) { + return new UnifiedTestSuiteBuilder(title); + } + + /** + * Establish common defaults + * - id and name = client0, listens for commandStartedEvent + * - id and name = database0 + * - id and name = collection0 + */ + static get defaultEntities(): EntityDescription[] { + return [ + { + client: { + id: 'client0', + useMultipleMongoses: true, + observeEvents: ['commandStartedEvent'] + } + }, + { + database: { + id: 'database0', + client: 'client0', + databaseName: 'database0' + } + }, + { + collection: { + id: 'collection0', + database: 'database0', + collectionName: 'collection0' + } + } + ]; + } + constructor(description: string) { this._description = description; + this._createEntities = []; } description(description: string): this { @@ -400,14 +426,6 @@ export class UnifiedTestSuiteBuilder { return this; } - /** - * sets the database name for the tests - */ - databaseName(name: string): this { - this._databaseName = name; - return this; - } - runOnRequirement(requirement: RunOnRequirement): this; runOnRequirement(requirement: RunOnRequirement[]): this; runOnRequirement(requirement: RunOnRequirement | RunOnRequirement[]): this { @@ -423,36 +441,24 @@ export class UnifiedTestSuiteBuilder { } toJSON(): UnifiedSuite { - const databaseName = - this._databaseName !== '' - ? this._databaseName - : this._description - .split(' ') - .filter(s => s.length > 0) - .join('_'); return { description: this._description, schemaVersion: this._schemaVersion, runOnRequirements: this._runOnRequirement, - createEntities: this._createEntities.map(entity => { - if ('database' in entity) { - return { - database: { ...entity.database, databaseName } - }; - } - - return entity; - }), - initialData: this._initialData.map(data => { - return { - ...data, - databaseName - }; - }), + createEntities: this._createEntities, + initialData: this._initialData, tests: this._tests }; } + run(): void { + return runUnifiedSuite([this.toJSON()]); + } + + toMocha() { + return describe(this._description, () => runUnifiedSuite([this.toJSON()])); + } + clone(): UnifiedSuite { return JSON.parse(JSON.stringify(this)); }