diff --git a/test/functional/causal_consistency.test.js b/test/integration/causal-consistency/causal_consistency.test.js similarity index 98% rename from test/functional/causal_consistency.test.js rename to test/integration/causal-consistency/causal_consistency.test.js index 9aac766d1cd..bfca7efb447 100644 --- a/test/functional/causal_consistency.test.js +++ b/test/integration/causal-consistency/causal_consistency.test.js @@ -1,9 +1,9 @@ 'use strict'; -const { LEGACY_HELLO_COMMAND } = require('../../src/constants'); +const { LEGACY_HELLO_COMMAND } = require('../../../src/constants'); -const setupDatabase = require('./shared').setupDatabase, - expect = require('chai').expect; +const { setupDatabase } = require('../shared'); +const { expect } = require('chai'); const ignoredCommands = [LEGACY_HELLO_COMMAND, 'endSessions']; const test = { commands: { started: [], succeeded: [] } }; diff --git a/test/functional/change_stream.test.js b/test/integration/change-streams/change_stream.test.js similarity index 99% rename from test/functional/change_stream.test.js rename to test/integration/change-streams/change_stream.test.js index a4b814ad228..e60c1bbf202 100644 --- a/test/functional/change_stream.test.js +++ b/test/integration/change-streams/change_stream.test.js @@ -1,22 +1,21 @@ 'use strict'; const assert = require('assert'); const { Transform, PassThrough } = require('stream'); -const { MongoNetworkError } = require('../../src/error'); -const { delay, setupDatabase, withClient, withCursor } = require('./shared'); -const mock = require('../tools/mongodb-mock/index'); -const { EventCollector, getSymbolFrom } = require('../tools/utils'); +const { delay, setupDatabase, withClient, withCursor } = require('../shared'); +const mock = require('../../tools/mongodb-mock/index'); +const { EventCollector, getSymbolFrom } = require('../../tools/utils'); const chai = require('chai'); const expect = chai.expect; const sinon = require('sinon'); -const { ObjectId, Timestamp, Long, ReadPreference } = require('../../src'); +const { ObjectId, Timestamp, Long, ReadPreference, MongoNetworkError } = require('../../../src'); const fs = require('fs'); const os = require('os'); const path = require('path'); const crypto = require('crypto'); -const { LEGACY_HELLO_COMMAND } = require('../../src/constants'); +const { LEGACY_HELLO_COMMAND } = require('../../../src/constants'); chai.use(require('chai-subset')); -const { isHello } = require('../../src/utils'); +const { isHello } = require('../../../src/utils'); function withChangeStream(dbName, collectionName, callback) { if (arguments.length === 1) { diff --git a/test/functional/change_stream_spec.test.js b/test/integration/change-streams/change_streams.spec.test.js similarity index 96% rename from test/functional/change_stream_spec.test.js rename to test/integration/change-streams/change_streams.spec.test.js index 622627ca4bb..82f54ffd6d7 100644 --- a/test/functional/change_stream_spec.test.js +++ b/test/integration/change-streams/change_streams.spec.test.js @@ -1,15 +1,12 @@ 'use strict'; const path = require('path'); -const chai = require('chai'); -const { loadSpecTests } = require('../spec'); -const { runUnifiedSuite } = require('../tools/unified-spec-runner/runner'); +const { expect } = require('chai'); +const { loadSpecTests } = require('../../spec'); +const { runUnifiedSuite } = require('../../tools/unified-spec-runner/runner'); const camelCase = require('lodash.camelcase'); -const { setupDatabase } = require('./shared'); -const { delay } = require('./shared'); - -const expect = chai.expect; -const { LEGACY_HELLO_COMMAND } = require('../../src/constants'); +const { LEGACY_HELLO_COMMAND } = require('../../../src/constants'); +const { delay, setupDatabase } = require('../shared'); describe('Change Streams Spec - Unified', function () { runUnifiedSuite(loadSpecTests(path.join('change-streams', 'unified'))); diff --git a/test/functional/collection_management_spec.test.js b/test/integration/collection-management/collection_management.spec.test.js similarity index 82% rename from test/functional/collection_management_spec.test.js rename to test/integration/collection-management/collection_management.spec.test.js index e55567d7d66..622c68957b6 100644 --- a/test/functional/collection_management_spec.test.js +++ b/test/integration/collection-management/collection_management.spec.test.js @@ -1,8 +1,8 @@ 'use strict'; const { expect } = require('chai'); -const { loadSpecTests } = require('../spec/index'); -const { runUnifiedTest } = require('../tools/unified-spec-runner/runner'); +const { loadSpecTests } = require('../../spec/index'); +const { runUnifiedTest } = require('../../tools/unified-spec-runner/runner'); describe('Collection management unified spec tests', function () { for (const collectionManagementTest of loadSpecTests('collection-management')) { diff --git a/test/integration/command-monitoring/command_monitoring.spec.test.js b/test/integration/command-monitoring/command_monitoring.spec.test.js new file mode 100644 index 00000000000..ee5044e1e30 --- /dev/null +++ b/test/integration/command-monitoring/command_monitoring.spec.test.js @@ -0,0 +1,295 @@ +'use strict'; + +const { setupDatabase, filterOutCommands } = require('../shared'); +const { loadSpecTests } = require('../../spec'); +const { LEGACY_HELLO_COMMAND } = require('../../../src/constants'); +const { expect } = require('chai'); +const { runUnifiedTest } = require('../../tools/unified-spec-runner/runner'); + +describe('Command Monitoring spec tests', function () { + describe('command monitoring legacy spec tests', function () { + before(function () { + return setupDatabase(this.configuration); + }); + + // TODO: The worst part about this custom validation method is that it does not + // provide the rich context of failure location that chai gives us out of + // the box. I investigated extending chai, however their internal implementation + // does not reuse other internal methods, so we'd have to bring lodash in. + // It may be worth seeing if we can improve on this, as we might need the + // behavior in other future YAML tests. + const maybeLong = val => (typeof val.equals === 'function' ? val.toNumber() : val); + function apmExpect(actual, expected, parentKey) { + Object.keys(expected).forEach(key => { + expect(actual).to.include.key(key); + + // TODO: This is a workaround that works because all sorts in the specs + // are objects with one key; ideally we'd want to adjust the spec definitions + // to indicate whether order matters for any given key and set general + // expectations accordingly (see NODE-3235) + if (key === 'sort') { + expect(actual[key]).to.be.instanceOf(Map); + expect(Object.keys(expected[key])).to.have.lengthOf(1); + expect(actual[key].size).to.equal(1); + expect(actual[key].get(Object.keys(expected[key])[0])).to.equal( + Object.values(expected[key])[0] + ); + return; + } + + if (Array.isArray(expected[key])) { + expect(actual[key]).to.be.instanceOf(Array); + expect(actual[key]).to.have.lengthOf(expected[key].length); + for (let i = 0; i < expected[key].length; ++i) { + apmExpect(actual[key][i], expected[key][i], key); + } + + return; + } + + if (expected[key] === 42 || expected[key] === '42' || expected[key] === '') { + if (key === 'code' && expected[key] === 42) { + expect(actual[key]).to.be.greaterThan(0); + } + + if (key === 'errmsg' && expected[key] === '') { + expect(actual[key]).to.have.lengthOf.at.least(1); // >= 1 + } + + if (key === 'getmore' || (parentKey === 'cursor' && key === 'id')) { + expect(maybeLong(actual[key])).to.be.greaterThan(0); + } + + return; + } + + // cheap isPlainObject clone + if (Object.prototype.toString.call(expected[key]) === '[object Object]') { + apmExpect(actual[key], expected[key], key); + return; + } + + // otherwise compare the values + expect(maybeLong(actual[key]), key).to.deep.equal(expected[key]); + }); + } + + function validateCommandStartedEvent(expected, event) { + expect(event.commandName).to.equal(expected.command_name); + expect(event.databaseName).to.equal(expected.database_name); + apmExpect(event.command, expected.command); + } + + function validateCommandSucceededEvent(expected, event) { + expect(event.commandName).to.equal(expected.command_name); + apmExpect(event.reply, expected.reply); + } + + function validateCommandFailedEvent(expected, event) { + expect(event.commandName).to.equal(expected.command_name); + } + + function validateExpectations(expectation, results) { + if (expectation.command_started_event) { + validateCommandStartedEvent(expectation.command_started_event, results.starts.shift()); + } else if (expectation.command_succeeded_event) { + validateCommandSucceededEvent( + expectation.command_succeeded_event, + results.successes.shift() + ); + } else if (expectation.command_failed_event) { + validateCommandFailedEvent(expectation.command_failed_event, results.failures.shift()); + } + } + + function executeOperation(client, scenario, test) { + // Get the operation + const operation = test.operation; + // Get the command name + const commandName = operation.name; + // Get the arguments + const args = operation.arguments || {}; + // Get the database instance + const db = client.db(scenario.database_name); + // Get the collection + const collection = db.collection(scenario.collection_name); + // Parameters + const params = []; + // Options + let options = null; + // Get the data + const data = scenario.data; + // Command Monitoring context + const monitoringResults = { + successes: [], + failures: [], + starts: [] + }; + + // Drop the collection + return collection + .drop() + .catch(err => { + // potentially skip this error + if (!err.message.match(/ns not found/)) throw err; + }) + .then(() => collection.insertMany(data)) + .then(r => { + expect(data).to.have.length(Object.keys(r.insertedIds).length); + + // Set up the listeners + client.on( + 'commandStarted', + filterOutCommands([LEGACY_HELLO_COMMAND, 'endSessions'], monitoringResults.starts) + ); + client.on( + 'commandFailed', + filterOutCommands([LEGACY_HELLO_COMMAND, 'endSessions'], monitoringResults.failures) + ); + client.on( + 'commandSucceeded', + filterOutCommands([LEGACY_HELLO_COMMAND, 'endSessions'], monitoringResults.successes) + ); + + // Unpack the operation + if (args.options) options = args.options; + if (args.filter) params.push(args.filter); + if (args.deletes) params.push(args.deletes); + if (args.document) params.push(args.document); + if (args.documents) params.push(args.documents); + if (args.update) params.push(args.update); + if (args.requests) { + if (operation.name !== 'bulkWrite') { + params.push(args.requests); + } else { + params.push( + args.requests.map(r => { + return { [r.name]: r.arguments.document || r.arguments }; + }) + ); + } + } + + if (args.writeConcern) { + options = options || {}; + options.writeConcern = args.writeConcern; + } + + if (typeof args.ordered === 'boolean') { + if (options == null) { + options = { ordered: args.ordered }; + } else { + options.ordered = args.ordered; + } + } + + if (typeof args.upsert === 'boolean') { + if (options == null) { + options = { upsert: args.upsert }; + } else { + options.upsert = args.upsert; + } + } + + // Find command is special needs to executed using toArray + if (operation.name === 'find') { + let cursor = collection[commandName](); + + // Set the options + if (args.filter) cursor = cursor.filter(args.filter); + if (args.batchSize) cursor = cursor.batchSize(args.batchSize); + if (args.limit) cursor = cursor.limit(args.limit); + if (args.skip) cursor = cursor.skip(args.skip); + if (args.sort) cursor = cursor.sort(args.sort); + + // Set any modifiers + if (args.modifiers) { + for (let modifier in args.modifiers) { + cursor.addQueryModifier(modifier, args.modifiers[modifier]); + } + } + + // Execute find + return cursor + .toArray() + .catch(() => {} /* ignore */) + .then(() => + test.expectations.forEach(expectation => + validateExpectations(expectation, monitoringResults) + ) + ); + } + // Add options if they exists + if (options) params.push(options); + + // Execute the operation + const coll = operation.collectionOptions + ? db.collection(scenario.collection_name, operation.collectionOptions) + : db.collection(scenario.collection_name); + + const promise = coll[commandName].apply(coll, params); + return promise + .catch(() => {} /* ignore */) + .then(() => + test.expectations.forEach(expectation => + validateExpectations(expectation, monitoringResults) + ) + ); + }); + } + + loadSpecTests('command-monitoring/legacy').forEach(scenario => { + if (scenario.name === 'command') return; // FIXME(NODE-3074): remove when `count` spec tests have been fixed + describe(scenario.name, function () { + scenario.tests.forEach(test => { + const requirements = { topology: ['single', 'replicaset', 'sharded'] }; + if (test.ignore_if_server_version_greater_than) { + requirements.mongodb = `<${test.ignore_if_server_version_greater_than}`; + } else if (test.ignore_if_server_version_less_than) { + requirements.mongodb = `>${test.ignore_if_server_version_less_than}`; + } + + if (test.ignore_if_topology_type) { + requirements.topology = requirements.topology.filter( + top => test.ignore_if_topology_type.indexOf(top) < 0 + ); + } + + it(test.description, { + metadata: { requires: requirements }, + test: function () { + if ( + test.description === + 'A successful find event with a getmore and the server kills the cursor' + ) { + this.skip(); + } + + const client = this.configuration.newClient({}, { monitorCommands: true }); + return client.connect().then(client => { + expect(client).to.exist; + return executeOperation(client, scenario, test).then(() => client.close()); + }); + } + }); + }); + }); + }); + }); + + describe('command monitoring unified spec tests', () => { + for (const loadedSpec of loadSpecTests('command-monitoring/unified')) { + expect(loadedSpec).to.include.all.keys(['description', 'tests']); + context(String(loadedSpec.description), function () { + for (const test of loadedSpec.tests) { + it(String(test.description), { + metadata: { sessions: { skipLeakTests: true } }, + test: async function () { + await runUnifiedTest(this, loadedSpec, test); + } + }); + } + }); + } + }); +}); diff --git a/test/functional/command_monitoring.test.js b/test/integration/command-monitoring/command_monitoring.test.js similarity index 68% rename from test/functional/command_monitoring.test.js rename to test/integration/command-monitoring/command_monitoring.test.js index 5a75f9f61f9..1ba645819d4 100644 --- a/test/functional/command_monitoring.test.js +++ b/test/integration/command-monitoring/command_monitoring.test.js @@ -1,16 +1,8 @@ 'use strict'; -const { - setupDatabase, - filterForCommands, - filterOutCommands, - ignoreNsNotFound -} = require('./shared'); -const { loadSpecTests } = require('../spec'); +const { setupDatabase, filterForCommands, ignoreNsNotFound } = require('../shared'); const { expect } = require('chai'); -const { ReadPreference } = require('../../src/read_preference'); -const { runUnifiedTest } = require('../tools/unified-spec-runner/runner'); -const { LEGACY_HELLO_COMMAND } = require('../../src/constants'); +const { ReadPreference } = require('../../../src/read_preference'); describe('APM', function () { before(function () { @@ -705,290 +697,4 @@ describe('APM', function () { }); }); }); - - describe('command monitoring spec tests', function () { - before(function () { - return setupDatabase(this.configuration); - }); - - // TODO: The worst part about this custom validation method is that it does not - // provide the rich context of failure location that chai gives us out of - // the box. I investigated extending chai, however their internal implementation - // does not reuse other internal methods, so we'd have to bring lodash in. - // It may be worth seeing if we can improve on this, as we might need the - // behavior in other future YAML tests. - const maybeLong = val => (typeof val.equals === 'function' ? val.toNumber() : val); - function apmExpect(actual, expected, parentKey) { - Object.keys(expected).forEach(key => { - expect(actual).to.include.key(key); - - // TODO: This is a workaround that works because all sorts in the specs - // are objects with one key; ideally we'd want to adjust the spec definitions - // to indicate whether order matters for any given key and set general - // expectations accordingly (see NODE-3235) - if (key === 'sort') { - expect(actual[key]).to.be.instanceOf(Map); - expect(Object.keys(expected[key])).to.have.lengthOf(1); - expect(actual[key].size).to.equal(1); - expect(actual[key].get(Object.keys(expected[key])[0])).to.equal( - Object.values(expected[key])[0] - ); - return; - } - - if (Array.isArray(expected[key])) { - expect(actual[key]).to.be.instanceOf(Array); - expect(actual[key]).to.have.lengthOf(expected[key].length); - for (let i = 0; i < expected[key].length; ++i) { - apmExpect(actual[key][i], expected[key][i], key); - } - - return; - } - - if (expected[key] === 42 || expected[key] === '42' || expected[key] === '') { - if (key === 'code' && expected[key] === 42) { - expect(actual[key]).to.be.greaterThan(0); - } - - if (key === 'errmsg' && expected[key] === '') { - expect(actual[key]).to.have.lengthOf.at.least(1); // >= 1 - } - - if (key === 'getmore' || (parentKey === 'cursor' && key === 'id')) { - expect(maybeLong(actual[key])).to.be.greaterThan(0); - } - - return; - } - - // cheap isPlainObject clone - if (Object.prototype.toString.call(expected[key]) === '[object Object]') { - apmExpect(actual[key], expected[key], key); - return; - } - - // otherwise compare the values - expect(maybeLong(actual[key]), key).to.deep.equal(expected[key]); - }); - } - - function validateCommandStartedEvent(expected, event) { - expect(event.commandName).to.equal(expected.command_name); - expect(event.databaseName).to.equal(expected.database_name); - apmExpect(event.command, expected.command); - } - - function validateCommandSucceededEvent(expected, event) { - expect(event.commandName).to.equal(expected.command_name); - apmExpect(event.reply, expected.reply); - } - - function validateCommandFailedEvent(expected, event) { - expect(event.commandName).to.equal(expected.command_name); - } - - function validateExpectations(expectation, results) { - if (expectation.command_started_event) { - validateCommandStartedEvent(expectation.command_started_event, results.starts.shift()); - } else if (expectation.command_succeeded_event) { - validateCommandSucceededEvent( - expectation.command_succeeded_event, - results.successes.shift() - ); - } else if (expectation.command_failed_event) { - validateCommandFailedEvent(expectation.command_failed_event, results.failures.shift()); - } - } - - function executeOperation(client, scenario, test) { - // Get the operation - const operation = test.operation; - // Get the command name - const commandName = operation.name; - // Get the arguments - const args = operation.arguments || {}; - // Get the database instance - const db = client.db(scenario.database_name); - // Get the collection - const collection = db.collection(scenario.collection_name); - // Parameters - const params = []; - // Options - let options = null; - // Get the data - const data = scenario.data; - // Command Monitoring context - const monitoringResults = { - successes: [], - failures: [], - starts: [] - }; - - // Drop the collection - return collection - .drop() - .catch(err => { - // potentially skip this error - if (!err.message.match(/ns not found/)) throw err; - }) - .then(() => collection.insertMany(data)) - .then(r => { - expect(data).to.have.length(Object.keys(r.insertedIds).length); - - // Set up the listeners - client.on( - 'commandStarted', - filterOutCommands([LEGACY_HELLO_COMMAND, 'endSessions'], monitoringResults.starts) - ); - client.on( - 'commandFailed', - filterOutCommands([LEGACY_HELLO_COMMAND, 'endSessions'], monitoringResults.failures) - ); - client.on( - 'commandSucceeded', - filterOutCommands([LEGACY_HELLO_COMMAND, 'endSessions'], monitoringResults.successes) - ); - - // Unpack the operation - if (args.options) options = args.options; - if (args.filter) params.push(args.filter); - if (args.deletes) params.push(args.deletes); - if (args.document) params.push(args.document); - if (args.documents) params.push(args.documents); - if (args.update) params.push(args.update); - if (args.requests) { - if (operation.name !== 'bulkWrite') { - params.push(args.requests); - } else { - params.push( - args.requests.map(r => { - return { [r.name]: r.arguments.document || r.arguments }; - }) - ); - } - } - - if (args.writeConcern) { - options = options || {}; - options.writeConcern = args.writeConcern; - } - - if (typeof args.ordered === 'boolean') { - if (options == null) { - options = { ordered: args.ordered }; - } else { - options.ordered = args.ordered; - } - } - - if (typeof args.upsert === 'boolean') { - if (options == null) { - options = { upsert: args.upsert }; - } else { - options.upsert = args.upsert; - } - } - - // Find command is special needs to executed using toArray - if (operation.name === 'find') { - let cursor = collection[commandName](); - - // Set the options - if (args.filter) cursor = cursor.filter(args.filter); - if (args.batchSize) cursor = cursor.batchSize(args.batchSize); - if (args.limit) cursor = cursor.limit(args.limit); - if (args.skip) cursor = cursor.skip(args.skip); - if (args.sort) cursor = cursor.sort(args.sort); - - // Set any modifiers - if (args.modifiers) { - for (let modifier in args.modifiers) { - cursor.addQueryModifier(modifier, args.modifiers[modifier]); - } - } - - // Execute find - return cursor - .toArray() - .catch(() => {} /* ignore */) - .then(() => - test.expectations.forEach(expectation => - validateExpectations(expectation, monitoringResults) - ) - ); - } - // Add options if they exists - if (options) params.push(options); - - // Execute the operation - const coll = operation.collectionOptions - ? db.collection(scenario.collection_name, operation.collectionOptions) - : db.collection(scenario.collection_name); - - const promise = coll[commandName].apply(coll, params); - return promise - .catch(() => {} /* ignore */) - .then(() => - test.expectations.forEach(expectation => - validateExpectations(expectation, monitoringResults) - ) - ); - }); - } - - loadSpecTests('command-monitoring/legacy').forEach(scenario => { - if (scenario.name === 'command') return; // FIXME(NODE-3074): remove when `count` spec tests have been fixed - describe(scenario.name, function () { - scenario.tests.forEach(test => { - const requirements = { topology: ['single', 'replicaset', 'sharded'] }; - if (test.ignore_if_server_version_greater_than) { - requirements.mongodb = `<${test.ignore_if_server_version_greater_than}`; - } else if (test.ignore_if_server_version_less_than) { - requirements.mongodb = `>${test.ignore_if_server_version_less_than}`; - } - - if (test.ignore_if_topology_type) { - requirements.topology = requirements.topology.filter( - top => test.ignore_if_topology_type.indexOf(top) < 0 - ); - } - - it(test.description, { - metadata: { requires: requirements }, - test: function () { - if ( - test.description === - 'A successful find event with a getmore and the server kills the cursor' - ) { - this.skip(); - } - - const client = this.configuration.newClient({}, { monitorCommands: true }); - return client.connect().then(client => { - expect(client).to.exist; - return executeOperation(client, scenario, test).then(() => client.close()); - }); - } - }); - }); - }); - }); - }); - - describe('command monitoring unified spec tests', () => { - for (const loadedSpec of loadSpecTests('command-monitoring/unified')) { - expect(loadedSpec).to.include.all.keys(['description', 'tests']); - context(String(loadedSpec.description), function () { - for (const test of loadedSpec.tests) { - it(String(test.description), { - metadata: { sessions: { skipLeakTests: true } }, - test: async function () { - await runUnifiedTest(this, loadedSpec, test); - } - }); - } - }); - } - }); }); diff --git a/test/functional/cmap/connection.test.js b/test/integration/connection-monitoring-and-pooling/connection.test.js similarity index 98% rename from test/functional/cmap/connection.test.js rename to test/integration/connection-monitoring-and-pooling/connection.test.js index a581ad5cca5..f237f4a20a1 100644 --- a/test/functional/cmap/connection.test.js +++ b/test/integration/connection-monitoring-and-pooling/connection.test.js @@ -3,7 +3,7 @@ const { Connection } = require('../../../src/cmap/connection'); const { connect } = require('../../../src/cmap/connect'); const { expect } = require('chai'); -const { setupDatabase } = require('../../functional/shared'); +const { setupDatabase } = require('../shared'); const { ns, HostAddress } = require('../../../src/utils'); const { LEGACY_HELLO_COMMAND } = require('../../../src/constants'); diff --git a/test/integration/read-write-concern/read_write_concern.spec.test.js b/test/integration/read-write-concern/read_write_concern.spec.test.js new file mode 100644 index 00000000000..0cb09cc1a74 --- /dev/null +++ b/test/integration/read-write-concern/read_write_concern.spec.test.js @@ -0,0 +1,18 @@ +'use strict'; + +const { TestRunnerContext, generateTopologyTests } = require('../../tools/spec-runner'); +const { loadSpecTests } = require('../../spec'); + +describe('Read Write Concern spec tests', function () { + describe('operation spec tests', function () { + const testContext = new TestRunnerContext(); + const testSuites = loadSpecTests('read-write-concern/operation'); + + after(() => testContext.teardown()); + before(function () { + return testContext.setup(this.configuration); + }); + + generateTopologyTests(testSuites, testContext); + }); +}); diff --git a/test/functional/write_concern.test.js b/test/integration/read-write-concern/write_concern.test.js similarity index 69% rename from test/functional/write_concern.test.js rename to test/integration/read-write-concern/write_concern.test.js index 26c6b027cd7..8f207bf822e 100644 --- a/test/functional/write_concern.test.js +++ b/test/integration/read-write-concern/write_concern.test.js @@ -1,33 +1,16 @@ 'use strict'; -const chai = require('chai'); - -const expect = chai.expect; -const TestRunnerContext = require('./spec-runner').TestRunnerContext; -const generateTopologyTests = require('./spec-runner').generateTopologyTests; -const loadSpecTests = require('../spec').loadSpecTests; -const { withMonitoredClient } = require('./shared'); -const { LEGACY_HELLO_COMMAND } = require('../../src/constants'); +const { expect } = require('chai'); +const { withMonitoredClient } = require('../shared'); +const { LEGACY_HELLO_COMMAND } = require('../../../src/constants'); // WriteConcernError test requires const { once } = require('events'); -const mock = require('../tools/mongodb-mock/index'); -const { MongoClient, MongoServerError } = require('../../src'); +const mock = require('../../tools/mongodb-mock/index'); +const { MongoClient, MongoServerError } = require('../../../src'); describe('Write Concern', function () { - describe('spec tests', function () { - const testContext = new TestRunnerContext(); - const testSuites = loadSpecTests('read-write-concern/operation'); - - after(() => testContext.teardown()); - before(function () { - return testContext.setup(this.configuration); - }); - - generateTopologyTests(testSuites, testContext); - }); - it( 'should respect writeConcern from uri', withMonitoredClient('insert', { queryOptions: { w: 0 } }, function (client, events, done) { @@ -89,38 +72,40 @@ describe('Write Concern', function () { ); }); - let server; - before(() => { - return mock.createServer().then(s => { - server = s; + describe('mock server write concern test', () => { + let server; + before(() => { + return mock.createServer().then(s => { + server = s; + }); }); - }); - after(() => mock.cleanup()); + after(() => mock.cleanup()); - it('should pipe writeConcern from client down to API call', function () { - server.setMessageHandler(request => { - if (request.document && request.document[LEGACY_HELLO_COMMAND]) { - return request.reply(mock.HELLO); - } - expect(request.document.writeConcern).to.exist; - expect(request.document.writeConcern.w).to.equal('majority'); - return request.reply({ ok: 1 }); - }); - - const uri = `mongodb://${server.uri()}`; - const client = new MongoClient(uri, { writeConcern: 'majority' }); - return client - .connect() - .then(() => { - const db = client.db('wc_test'); - const collection = db.collection('wc'); - - return collection.insertMany([{ a: 2 }]); - }) - .then(() => { - return client.close(); + it('should pipe writeConcern from client down to API call', function () { + server.setMessageHandler(request => { + if (request.document && request.document[LEGACY_HELLO_COMMAND]) { + return request.reply(mock.HELLO); + } + expect(request.document.writeConcern).to.exist; + expect(request.document.writeConcern.w).to.equal('majority'); + return request.reply({ ok: 1 }); }); + + const uri = `mongodb://${server.uri()}`; + const client = new MongoClient(uri, { writeConcern: 'majority' }); + return client + .connect() + .then(() => { + const db = client.db('wc_test'); + const collection = db.collection('wc'); + + return collection.insertMany([{ a: 2 }]); + }) + .then(() => { + return client.close(); + }); + }); }); // This test was moved from the WriteConcernError unit test file, there is probably a better place for it diff --git a/test/functional/sdam.test.js b/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.js similarity index 86% rename from test/functional/sdam.test.js rename to test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.js index 399978d1b44..4f05a4603be 100644 --- a/test/functional/sdam.test.js +++ b/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.js @@ -1,7 +1,6 @@ 'use strict'; -const TestRunnerContext = require('./spec-runner').TestRunnerContext; -const loadSpecTests = require('../spec').loadSpecTests; -const generateTopologyTests = require('./spec-runner').generateTopologyTests; +const { TestRunnerContext, generateTopologyTests } = require('../../tools/spec-runner'); +const { loadSpecTests } = require('../../spec'); class SDAMRunnerContext extends TestRunnerContext { constructor() { diff --git a/test/integration/unified-test-format/unified-runner.test.ts b/test/integration/unified-test-format/unified_test_format.spec.test.ts similarity index 100% rename from test/integration/unified-test-format/unified-runner.test.ts rename to test/integration/unified-test-format/unified_test_format.spec.test.ts diff --git a/test/functional/uri.test.js b/test/integration/uri-options/uri.test.js similarity index 97% rename from test/functional/uri.test.js rename to test/integration/uri-options/uri.test.js index 90787b9b4da..a6c8e63f478 100644 --- a/test/functional/uri.test.js +++ b/test/integration/uri-options/uri.test.js @@ -1,8 +1,8 @@ 'use strict'; -const expect = require('chai').expect; +const { expect } = require('chai'); const sinon = require('sinon'); -const { Topology } = require('../../src/sdam/topology'); +const { Topology } = require('../../../src/sdam/topology'); describe('URI', function () { it('should correctly allow for w:0 overriding on the connect url', { diff --git a/test/functional/uri_options_spec.test.js b/test/integration/uri-options/uri_options.spec.test.js similarity index 92% rename from test/functional/uri_options_spec.test.js rename to test/integration/uri-options/uri_options.spec.test.js index dd343223310..d580ac88b77 100644 --- a/test/functional/uri_options_spec.test.js +++ b/test/integration/uri-options/uri_options.spec.test.js @@ -4,9 +4,9 @@ const { expect } = require('chai'); const { promisify } = require('util'); require('chai').use(require('chai-subset')); -const { parseOptions, resolveSRVRecord } = require('../../src/connection_string'); -const { MongoParseError } = require('../../src/error'); -const { loadSpecTests } = require('../spec'); +const { parseOptions, resolveSRVRecord } = require('../../../src/connection_string'); +const { MongoParseError } = require('../../../src/error'); +const { loadSpecTests } = require('../../spec'); describe('URI Options (spec)', function () { const uriSpecs = loadSpecTests('uri-options'); diff --git a/test/unit/cmap/connection_monitoring_and_pooling.spec.test.js b/test/unit/cmap/connection_monitoring_and_pooling.spec.test.js new file mode 100644 index 00000000000..ebfe118d168 --- /dev/null +++ b/test/unit/cmap/connection_monitoring_and_pooling.spec.test.js @@ -0,0 +1,281 @@ +'use strict'; + +const util = require('util'); +const { loadSpecTests } = require('../../spec'); +const { ConnectionPool } = require('../../../src/cmap/connection_pool'); +const { EventEmitter } = require('events'); +const mock = require('../../tools/mongodb-mock/index'); +const { expect } = require('chai'); +const { isHello } = require('../../../src/utils'); + +const ALL_POOL_EVENTS = new Set([ + 'connectionPoolCreated', + 'connectionPoolClosed', + 'connectionCreated', + 'connectionReady', + 'connectionClosed', + 'connectionCheckOutStarted', + 'connectionCheckOutFailed', + 'connectionCheckedOut', + 'connectionCheckedIn', + 'connectionPoolCleared' +]); + +const PROMISIFIED_POOL_FUNCTIONS = { + checkOut: util.promisify(ConnectionPool.prototype.checkOut), + close: util.promisify(ConnectionPool.prototype.close) +}; + +function closePool(pool) { + return new Promise(resolve => { + ALL_POOL_EVENTS.forEach(ev => pool.removeAllListeners(ev)); + pool.close(resolve); + }); +} + +describe('Connection Monitoring and Pooling', function () { + let server; + after(() => mock.cleanup()); + before(() => mock.createServer().then(s => (server = s))); + + describe('spec tests', function () { + const threads = new Map(); + const connections = new Map(); + const orphans = new Set(); + const poolEvents = []; + const poolEventsEventEmitter = new EventEmitter(); + let pool = undefined; + + function createPool(options) { + options = Object.assign({}, options, { hostAddress: server.hostAddress() }); + pool = new ConnectionPool(options); + ALL_POOL_EVENTS.forEach(ev => { + pool.on(ev, x => { + poolEvents.push(x); + poolEventsEventEmitter.emit('poolEvent'); + }); + }); + } + + function getThread(name) { + let thread = threads.get(name); + if (!thread) { + thread = new Thread(); + threads.set(name, thread); + } + + return thread; + } + + function eventType(event) { + const eventName = event.constructor.name; + return eventName.substring(0, eventName.lastIndexOf('Event')); + } + + const OPERATION_FUNCTIONS = { + checkOut: function (op) { + return PROMISIFIED_POOL_FUNCTIONS.checkOut.call(pool).then(connection => { + if (op.label != null) { + connections.set(op.label, connection); + } else { + orphans.add(connection); + } + }); + }, + checkIn: function (op) { + const connection = connections.get(op.connection); + connections.delete(op.connection); + + if (!connection) { + throw new Error(`Attempted to release non-existient connection ${op.connection}`); + } + + return pool.checkIn(connection); + }, + clear: function () { + return pool.clear(); + }, + close: function () { + return PROMISIFIED_POOL_FUNCTIONS.close.call(pool); + }, + wait: function (options) { + const ms = options.ms; + return new Promise(r => setTimeout(r, ms)); + }, + start: function (options) { + const target = options.target; + const thread = getThread(target); + thread.start(); + }, + waitForThread: function (options) { + const name = options.name; + const target = options.target; + const suppressError = options.suppressError; + + const threadObj = threads.get(target); + + if (!threadObj) { + throw new Error(`Attempted to run op ${name} on non-existent thread ${target}`); + } + + return threadObj.finish().catch(e => { + if (!suppressError) { + throw e; + } + }); + }, + waitForEvent: function (options) { + const event = options.event; + const count = options.count; + return new Promise(resolve => { + function run() { + if (poolEvents.filter(ev => eventType(ev) === event).length >= count) { + return resolve(); + } + + poolEventsEventEmitter.once('poolEvent', run); + } + run(); + }); + } + }; + + class Thread { + constructor() { + this._killed = false; + this._error = undefined; + this._promise = new Promise(resolve => { + this.start = () => setTimeout(resolve); + }); + } + + run(op) { + if (this._killed || this._error) { + return; + } + + this._promise = this._promise + .then(() => this._runOperation(op)) + .catch(e => (this._error = e)); + } + + _runOperation(op) { + const operationFn = OPERATION_FUNCTIONS[op.name]; + if (!operationFn) { + throw new Error(`Invalid command ${op.name}`); + } + + return Promise.resolve() + .then(() => operationFn(op, this)) + .then(() => new Promise(r => setTimeout(r))); + } + + finish() { + this._killed = true; + return this._promise.then(() => { + if (this._error) { + throw this._error; + } + }); + } + } + + before(() => { + // we aren't testing errors yet, so it's fine for the mock server to just accept + // and establish valid connections + server.setMessageHandler(request => { + const doc = request.document; + if (isHello(doc)) { + request.reply(mock.HELLO); + } + }); + }); + + afterEach(() => { + const p = pool ? closePool(pool) : Promise.resolve(); + return p + .then(() => { + const connectionsToDestroy = Array.from(orphans).concat(Array.from(connections.values())); + const promises = connectionsToDestroy.map(conn => { + return new Promise((resolve, reject) => + conn.destroy({ force: true }, err => { + if (err) return reject(err); + resolve(); + }) + ); + }); + return Promise.all(promises); + }) + .then(() => { + pool = undefined; + threads.clear(); + connections.clear(); + orphans.clear(); + poolEvents.length = 0; + poolEventsEventEmitter.removeAllListeners(); + }); + }); + + loadSpecTests('connection-monitoring-and-pooling').forEach(test => { + it(test.description, function () { + const operations = test.operations; + const expectedEvents = test.events || []; + const ignoreEvents = test.ignore || []; + const expectedError = test.error; + const poolOptions = test.poolOptions || {}; + + let actualError; + + const MAIN_THREAD_KEY = Symbol('Main Thread'); + const mainThread = new Thread(); + threads.set(MAIN_THREAD_KEY, mainThread); + mainThread.start(); + + createPool(poolOptions); + + let basePromise = Promise.resolve(); + + for (let idx in operations) { + const op = operations[idx]; + + const threadKey = op.thread || MAIN_THREAD_KEY; + const thread = getThread(threadKey); + + basePromise = basePromise.then(() => { + if (!thread) { + throw new Error(`Invalid thread ${threadKey}`); + } + + return Promise.resolve() + .then(() => thread.run(op)) + .then(() => new Promise(r => setTimeout(r))); + }); + } + + return basePromise + .then(() => mainThread.finish()) + .catch(e => (actualError = e)) + .then(() => { + const actualEvents = poolEvents.filter(ev => ignoreEvents.indexOf(eventType(ev)) < 0); + + if (expectedError) { + expect(actualError).to.exist; + expect(actualError).property('message').to.equal(expectedError.message); + } else if (actualError) { + throw actualError; + } + + expectedEvents.forEach((expected, index) => { + const actual = actualEvents[index]; + if (expected.type) { + expect(actual.constructor.name).to.equal(`${expected.type}Event`); + delete expected.type; + } + + expect(actual).to.matchMongoSpec(expected); + }); + }); + }); + }); + }); +}); diff --git a/test/unit/cmap/connection_pool.test.js b/test/unit/cmap/connection_pool.test.js index d13912f603a..38e1f9c0e76 100644 --- a/test/unit/cmap/connection_pool.test.js +++ b/test/unit/cmap/connection_pool.test.js @@ -1,10 +1,7 @@ 'use strict'; -const util = require('util'); -const { loadSpecTests } = require('../../spec'); const { ConnectionPool } = require('../../../src/cmap/connection_pool'); const { WaitQueueTimeoutError } = require('../../../src/cmap/errors'); -const { EventEmitter } = require('events'); const mock = require('../../tools/mongodb-mock/index'); const cmapEvents = require('../../../src/cmap/connection_pool_events'); const sinon = require('sinon'); @@ -12,31 +9,6 @@ const { expect } = require('chai'); const { ns, isHello } = require('../../../src/utils'); const { LEGACY_HELLO_COMMAND } = require('../../../src/constants'); -const ALL_POOL_EVENTS = new Set([ - 'connectionPoolCreated', - 'connectionPoolClosed', - 'connectionCreated', - 'connectionReady', - 'connectionClosed', - 'connectionCheckOutStarted', - 'connectionCheckOutFailed', - 'connectionCheckedOut', - 'connectionCheckedIn', - 'connectionPoolCleared' -]); - -const PROMISIFIED_POOL_FUNCTIONS = { - checkOut: util.promisify(ConnectionPool.prototype.checkOut), - close: util.promisify(ConnectionPool.prototype.close) -}; - -function closePool(pool) { - return new Promise(resolve => { - ALL_POOL_EVENTS.forEach(ev => pool.removeAllListeners(ev)); - pool.close(resolve); - }); -} - describe('Connection Pool', function () { let server; after(() => mock.cleanup()); @@ -264,245 +236,4 @@ describe('Connection Pool', function () { }); }); }); - - describe('spec tests', function () { - const threads = new Map(); - const connections = new Map(); - const orphans = new Set(); - const poolEvents = []; - const poolEventsEventEmitter = new EventEmitter(); - let pool = undefined; - - function createPool(options) { - options = Object.assign({}, options, { hostAddress: server.hostAddress() }); - pool = new ConnectionPool(options); - ALL_POOL_EVENTS.forEach(ev => { - pool.on(ev, x => { - poolEvents.push(x); - poolEventsEventEmitter.emit('poolEvent'); - }); - }); - } - - function getThread(name) { - let thread = threads.get(name); - if (!thread) { - thread = new Thread(); - threads.set(name, thread); - } - - return thread; - } - - function eventType(event) { - const eventName = event.constructor.name; - return eventName.substring(0, eventName.lastIndexOf('Event')); - } - - const OPERATION_FUNCTIONS = { - checkOut: function (op) { - return PROMISIFIED_POOL_FUNCTIONS.checkOut.call(pool).then(connection => { - if (op.label != null) { - connections.set(op.label, connection); - } else { - orphans.add(connection); - } - }); - }, - checkIn: function (op) { - const connection = connections.get(op.connection); - connections.delete(op.connection); - - if (!connection) { - throw new Error(`Attempted to release non-existient connection ${op.connection}`); - } - - return pool.checkIn(connection); - }, - clear: function () { - return pool.clear(); - }, - close: function () { - return PROMISIFIED_POOL_FUNCTIONS.close.call(pool); - }, - wait: function (options) { - const ms = options.ms; - return new Promise(r => setTimeout(r, ms)); - }, - start: function (options) { - const target = options.target; - const thread = getThread(target); - thread.start(); - }, - waitForThread: function (options) { - const name = options.name; - const target = options.target; - const suppressError = options.suppressError; - - const threadObj = threads.get(target); - - if (!threadObj) { - throw new Error(`Attempted to run op ${name} on non-existent thread ${target}`); - } - - return threadObj.finish().catch(e => { - if (!suppressError) { - throw e; - } - }); - }, - waitForEvent: function (options) { - const event = options.event; - const count = options.count; - return new Promise(resolve => { - function run() { - if (poolEvents.filter(ev => eventType(ev) === event).length >= count) { - return resolve(); - } - - poolEventsEventEmitter.once('poolEvent', run); - } - run(); - }); - } - }; - - class Thread { - constructor() { - this._killed = false; - this._error = undefined; - this._promise = new Promise(resolve => { - this.start = () => setTimeout(resolve); - }); - } - - run(op) { - if (this._killed || this._error) { - return; - } - - this._promise = this._promise - .then(() => this._runOperation(op)) - .catch(e => (this._error = e)); - } - - _runOperation(op) { - const operationFn = OPERATION_FUNCTIONS[op.name]; - if (!operationFn) { - throw new Error(`Invalid command ${op.name}`); - } - - return Promise.resolve() - .then(() => operationFn(op, this)) - .then(() => new Promise(r => setTimeout(r))); - } - - finish() { - this._killed = true; - return this._promise.then(() => { - if (this._error) { - throw this._error; - } - }); - } - } - - before(() => { - // we aren't testing errors yet, so it's fine for the mock server to just accept - // and establish valid connections - server.setMessageHandler(request => { - const doc = request.document; - if (isHello(doc)) { - request.reply(mock.HELLO); - } - }); - }); - - afterEach(() => { - const p = pool ? closePool(pool) : Promise.resolve(); - return p - .then(() => { - const connectionsToDestroy = Array.from(orphans).concat(Array.from(connections.values())); - const promises = connectionsToDestroy.map(conn => { - return new Promise((resolve, reject) => - conn.destroy({ force: true }, err => { - if (err) return reject(err); - resolve(); - }) - ); - }); - return Promise.all(promises); - }) - .then(() => { - pool = undefined; - threads.clear(); - connections.clear(); - orphans.clear(); - poolEvents.length = 0; - poolEventsEventEmitter.removeAllListeners(); - }); - }); - - loadSpecTests('connection-monitoring-and-pooling').forEach(test => { - it(test.description, function () { - const operations = test.operations; - const expectedEvents = test.events || []; - const ignoreEvents = test.ignore || []; - const expectedError = test.error; - const poolOptions = test.poolOptions || {}; - - let actualError; - - const MAIN_THREAD_KEY = Symbol('Main Thread'); - const mainThread = new Thread(); - threads.set(MAIN_THREAD_KEY, mainThread); - mainThread.start(); - - createPool(poolOptions); - - let basePromise = Promise.resolve(); - - for (let idx in operations) { - const op = operations[idx]; - - const threadKey = op.thread || MAIN_THREAD_KEY; - const thread = getThread(threadKey); - - basePromise = basePromise.then(() => { - if (!thread) { - throw new Error(`Invalid thread ${threadKey}`); - } - - return Promise.resolve() - .then(() => thread.run(op)) - .then(() => new Promise(r => setTimeout(r))); - }); - } - - return basePromise - .then(() => mainThread.finish()) - .catch(e => (actualError = e)) - .then(() => { - const actualEvents = poolEvents.filter(ev => ignoreEvents.indexOf(eventType(ev)) < 0); - - if (expectedError) { - expect(actualError).to.exist; - expect(actualError).property('message').to.equal(expectedError.message); - } else if (actualError) { - throw actualError; - } - - expectedEvents.forEach((expected, index) => { - const actual = actualEvents[index]; - if (expected.type) { - expect(actual.constructor.name).to.equal(`${expected.type}Event`); - delete expected.type; - } - - expect(actual).to.matchMongoSpec(expected); - }); - }); - }); - }); - }); }); diff --git a/test/unit/connection_string.spec.test.ts b/test/unit/connection_string.spec.test.ts new file mode 100644 index 00000000000..17fd138becf --- /dev/null +++ b/test/unit/connection_string.spec.test.ts @@ -0,0 +1,99 @@ +import { expect } from 'chai'; + +import { parseOptions } from '../../src/connection_string'; +import { loadSpecTests } from '../spec'; + +// NOTE: These are cases we could never check for unless we write our own +// url parser. The node parser simply won't let these through, so we +// are safe skipping them. +const skipTests = [ + 'Invalid port (negative number) with hostname', + 'Invalid port (non-numeric string) with hostname', + 'Missing delimiting slash between hosts and options', + + // These tests are only relevant to the native driver which + // cares about specific keys, and validating their values + 'Unrecognized option keys are ignored', + 'Unsupported option values are ignored', + + // We don't actually support `wtimeoutMS` which this test depends upon + 'Deprecated (or unknown) options are ignored if replacement exists' +]; + +describe('Connection String spec tests', function () { + const suites = loadSpecTests('connection-string').concat(loadSpecTests('auth')); + + for (const suite of suites) { + describe(suite.name, function () { + for (const test of suite.tests) { + it(`${test.description}`, function () { + if (skipTests.includes(test.description)) { + return this.skip(); + } + + const message = `"${test.uri}"`; + + const valid = test.valid; + if (valid) { + const options = parseOptions(test.uri); + expect(options, message).to.be.ok; + + if (test.hosts) { + for (const [index, { host, port }] of test.hosts.entries()) { + expect(options.hosts[index], message).to.satisfy(e => { + return e.host === host || e.socketPath === host; + }); + if (typeof port === 'number') expect(options.hosts[index].port).to.equal(port); + } + } + + if (test.auth && test.auth.db != null) { + expect(options.dbName, message).to.equal(test.auth.db); + } + + if (test.auth && test.auth.username) { + expect(options.credentials, message).to.exist; + + if (test.auth.db != null) { + expect(options.credentials.source, message).to.equal(test.auth.db); + } + + if (test.auth.username != null) { + expect(options.credentials.username, message).to.equal(test.auth.username); + } + + if (test.auth.password != null) { + expect(options.credentials.password, message).to.equal(test.auth.password); + } + } + + if (test.options) { + for (const [optionKey, optionValue] of Object.entries(test.options)) { + switch (optionKey) { + case 'authmechanism': + expect(options.credentials.mechanism, message).to.eq(optionValue); + break; + case 'authmechanismproperties': + expect(options.credentials.mechanismProperties, message).to.deep.eq( + optionValue + ); + break; + case 'replicaset': + expect(options.replicaSet, message).to.equal(optionValue); + break; + case 'w': + expect(options.writeConcern.w).to.equal(optionValue); + break; + default: + throw Error(`This options is not covered by the spec test: ${optionKey}`); + } + } + } + } else { + expect(() => parseOptions(test.uri), message).to.throw(); + } + }); + } + }); + } +}); diff --git a/test/unit/connection_string.test.ts b/test/unit/connection_string.test.ts index a5152b526bf..171284ad4e2 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -8,24 +8,6 @@ import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from '../../src/cmap/auth import { parseOptions, resolveSRVRecord } from '../../src/connection_string'; import { MongoDriverError, MongoInvalidArgumentError, MongoParseError } from '../../src/error'; import { MongoOptions } from '../../src/mongo_client'; -import { loadSpecTests } from '../spec'; - -// NOTE: These are cases we could never check for unless we write our own -// url parser. The node parser simply won't let these through, so we -// are safe skipping them. -const skipTests = [ - 'Invalid port (negative number) with hostname', - 'Invalid port (non-numeric string) with hostname', - 'Missing delimiting slash between hosts and options', - - // These tests are only relevant to the native driver which - // cares about specific keys, and validating their values - 'Unrecognized option keys are ignored', - 'Unsupported option values are ignored', - - // We don't actually support `wtimeoutMS` which this test depends upon - 'Deprecated (or unknown) options are ignored if replacement exists' -]; describe('Connection String', function () { it('should not support auth passed with user', function () { @@ -240,84 +222,6 @@ describe('Connection String', function () { }); }); - describe('spec tests', function () { - const suites = loadSpecTests('connection-string').concat(loadSpecTests('auth')); - - for (const suite of suites) { - describe(suite.name, function () { - for (const test of suite.tests) { - it(`${test.description}`, function () { - if (skipTests.includes(test.description)) { - return this.skip(); - } - - const message = `"${test.uri}"`; - - const valid = test.valid; - if (valid) { - const options = parseOptions(test.uri); - expect(options, message).to.be.ok; - - if (test.hosts) { - for (const [index, { host, port }] of test.hosts.entries()) { - expect(options.hosts[index], message).to.satisfy(e => { - return e.host === host || e.socketPath === host; - }); - if (typeof port === 'number') expect(options.hosts[index].port).to.equal(port); - } - } - - if (test.auth && test.auth.db != null) { - expect(options.dbName, message).to.equal(test.auth.db); - } - - if (test.auth && test.auth.username) { - expect(options.credentials, message).to.exist; - - if (test.auth.db != null) { - expect(options.credentials.source, message).to.equal(test.auth.db); - } - - if (test.auth.username != null) { - expect(options.credentials.username, message).to.equal(test.auth.username); - } - - if (test.auth.password != null) { - expect(options.credentials.password, message).to.equal(test.auth.password); - } - } - - if (test.options) { - for (const [optionKey, optionValue] of Object.entries(test.options)) { - switch (optionKey) { - case 'authmechanism': - expect(options.credentials.mechanism, message).to.eq(optionValue); - break; - case 'authmechanismproperties': - expect(options.credentials.mechanismProperties, message).to.deep.eq( - optionValue - ); - break; - case 'replicaset': - expect(options.replicaSet, message).to.equal(optionValue); - break; - case 'w': - expect(options.writeConcern.w).to.equal(optionValue); - break; - default: - throw Error(`This options is not covered by the spec test: ${optionKey}`); - } - } - } - } else { - expect(() => parseOptions(test.uri), message).to.throw(); - } - }); - } - }); - } - }); - describe('mongodb+srv', function () { it('should parse a default database', function () { const options = parseOptions('mongodb+srv://test1.test.build.10gen.cc/somedb');