From 612bb693c791cc3e2bb163b2ee53e01914ac0e37 Mon Sep 17 00:00:00 2001 From: emadum Date: Mon, 8 Feb 2021 20:54:13 -0500 Subject: [PATCH 01/13] feat: versioned api --- .evergreen/generate_evergreen_tasks.js | 1 + src/cmap/connection.ts | 13 +- src/index.ts | 2 + src/mongo_client.ts | 25 +- src/operations/connect.ts | 2 +- src/sdam/server.ts | 2 + src/sdam/topology.ts | 12 +- src/utils.ts | 7 + .../unified-spec-runner/entities.ts | 4 +- test/functional/unified-spec-runner/match.ts | 4 +- test/functional/unified-spec-runner/runner.ts | 142 +++ .../unified-spec-runner/unified-utils.ts | 2 +- .../unified-spec-runner/unified.test.ts | 143 +-- test/functional/versioned-api.test.js | 28 + .../crud-api-version-1-strict.json | 1076 +++++++++++++++++ .../crud-api-version-1-strict.yml | 395 ++++++ .../versioned-api/crud-api-version-1.json | 1067 ++++++++++++++++ .../spec/versioned-api/crud-api-version-1.yml | 388 ++++++ ...ommand-helper-no-api-version-declared.json | 117 ++ ...command-helper-no-api-version-declared.yml | 67 + .../versioned-api/transaction-handling.json | 388 ++++++ .../versioned-api/transaction-handling.yml | 140 +++ test/tools/runner/config.js | 6 +- test/tools/runner/index.js | 3 +- 24 files changed, 3881 insertions(+), 153 deletions(-) create mode 100644 test/functional/unified-spec-runner/runner.ts create mode 100644 test/functional/versioned-api.test.js create mode 100644 test/spec/versioned-api/crud-api-version-1-strict.json create mode 100644 test/spec/versioned-api/crud-api-version-1-strict.yml create mode 100644 test/spec/versioned-api/crud-api-version-1.json create mode 100644 test/spec/versioned-api/crud-api-version-1.yml create mode 100644 test/spec/versioned-api/runcommand-helper-no-api-version-declared.json create mode 100644 test/spec/versioned-api/runcommand-helper-no-api-version-declared.yml create mode 100644 test/spec/versioned-api/transaction-handling.json create mode 100644 test/spec/versioned-api/transaction-handling.yml diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index 3320cf3b4f0..f45e40095b8 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -60,6 +60,7 @@ const OPERATING_SYSTEMS = [ ) ); +// TODO: NODE-3060: enable skipped tests on windows const WINDOWS_SKIP_TAGS = new Set(['atlas-connect', 'auth']); const TASKS = []; diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index bc6189e651c..3063444c07f 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -11,7 +11,8 @@ import { Callback, MongoDBNamespace, maxWireVersion, - HostAddress + HostAddress, + applyServerApiVersion } from '../utils'; import { AnyError, @@ -38,7 +39,7 @@ import { applyCommonQueryOptions, getReadPreference, isSharded } from './wire_pr import { ReadPreference, ReadPreferenceLike } from '../read_preference'; import { isTransactionCommand } from '../transactions'; import type { W, WriteConcern, WriteConcernOptions } from '../write_concern'; -import type { SupportedNodeConnectionOptions } from '../mongo_client'; +import type { ServerApi, SupportedNodeConnectionOptions } from '../mongo_client'; const kStream = Symbol('stream'); const kQueue = Symbol('queue'); @@ -107,6 +108,7 @@ export interface ConnectionOptions hostAddress: HostAddress; // Settings autoEncrypter?: AutoEncrypter; + serverApi?: ServerApi; monitorCommands: boolean; connectionType: typeof Connection; credentials?: MongoCredentials; @@ -136,6 +138,7 @@ export class Connection extends EventEmitter { closed: boolean; destroyed: boolean; lastIsMasterMS?: number; + serverApi?: ServerApi; /** @internal */ [kDescription]: StreamDescription; /** @internal */ @@ -168,6 +171,7 @@ export class Connection extends EventEmitter { this.address = streamIdentifier(stream); this.socketTimeout = options.socketTimeout ?? 0; this.monitorCommands = options.monitorCommands; + this.serverApi = options.serverApi; this.closed = false; this.destroyed = false; @@ -362,6 +366,11 @@ export class Connection extends EventEmitter { : new Query(cmdNs, finalCmd, commandOptions); const inTransaction = session && (session.inTransaction() || isTransactionCommand(finalCmd)); + + if (!inTransaction && !finalCmd.getMore && this.serverApi) { + applyServerApiVersion(finalCmd, this.serverApi); + } + const commandResponseHandler = inTransaction ? (err?: AnyError, ...args: Document[]) => { // We need to add a TransientTransactionError errorLabel, as stated in the transaction spec. diff --git a/src/index.ts b/src/index.ts index 798c223d5de..5e295967f01 100644 --- a/src/index.ts +++ b/src/index.ts @@ -170,6 +170,8 @@ export type { Auth, DriverInfo, MongoOptions, + ServerApi, + ServerApiVersion, SupportedNodeConnectionOptions, SupportedTLSConnectionOptions, SupportedTLSSocketOptions, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 458dd219ff6..0714a33c360 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -41,6 +41,17 @@ export const LogLevel = { debug: 'debug' } as const; +/** @public */ +export enum ServerApiVersion { + v1 = '1' +} + +export interface ServerApi { + version: string | ServerApiVersion; + strict?: boolean; + deprecationErrors?: boolean; +} + /** @public */ export type LogLevelId = typeof LogLevel[keyof typeof LogLevel]; @@ -237,6 +248,7 @@ export interface MongoClientPrivate { } const kOptions = Symbol('options'); +const kServerApi = Symbol('serverApi'); /** * The **MongoClient** class is a class that allows for making Connections to MongoDB. @@ -289,17 +301,24 @@ export class MongoClient extends EventEmitter { */ [kOptions]: MongoOptions; + /** + * The MongoDB Server API version + * @internal + * */ + [kServerApi]: ServerApi; + // debugging originalUri; originalOptions; - constructor(url: string, options?: MongoClientOptions) { + constructor(url: string, options?: MongoClientOptions, serverApi?: ServerApi) { super(); this.originalUri = url; this.originalOptions = options; this[kOptions] = parseOptions(url, this, options); + this[kServerApi] = Object.freeze({ version: ServerApiVersion.v1, ...serverApi }); // The internal state this.s = { @@ -319,6 +338,10 @@ export class MongoClient extends EventEmitter { return Object.freeze({ ...this[kOptions] }); } + get serverApi(): Readonly { + return this[kServerApi]; + } + get autoEncrypter(): AutoEncrypter | undefined { return this[kOptions].autoEncrypter; } diff --git a/src/operations/connect.ts b/src/operations/connect.ts index f1b10839c04..1883278fef8 100644 --- a/src/operations/connect.ts +++ b/src/operations/connect.ts @@ -153,7 +153,7 @@ function createTopology( callback: Callback ) { // Create the topology - const topology = new Topology(options.hosts, options); + const topology = new Topology(options.hosts, options, mongoClient.serverApi); // Events can be emitted before initialization is complete so we have to // save the reference to the topology on the client ASAP if the event handlers need to access it mongoClient.topology = topology; diff --git a/src/sdam/server.ts b/src/sdam/server.ts index d72dc6d619f..7c495b6896c 100644 --- a/src/sdam/server.ts +++ b/src/sdam/server.ts @@ -131,6 +131,8 @@ export class Server extends EventEmitter { constructor(topology: Topology, description: ServerDescription, options: ServerOptions) { super(); + options.serverApi = topology.serverApi; + const poolOptions = { hostAddress: description.hostAddress, ...options }; this.s = { diff --git a/src/sdam/topology.ts b/src/sdam/topology.ts index c4d81bd3ba8..58f5f245af6 100644 --- a/src/sdam/topology.ts +++ b/src/sdam/topology.ts @@ -50,7 +50,7 @@ import type { MongoCredentials } from '../cmap/auth/mongo_credentials'; import type { Transaction } from '../transactions'; import type { CloseOptions } from '../cmap/connection_pool'; import { DestroyOptions, Connection } from '../cmap/connection'; -import type { MongoClientOptions } from '../mongo_client'; +import type { MongoClientOptions, ServerApi } from '../mongo_client'; import { DEFAULT_OPTIONS } from '../connection_string'; import { serialize, deserialize } from '../bson'; @@ -170,6 +170,8 @@ export class Topology extends EventEmitter { ismaster?: Document; /** @internal */ _type?: string; + /** @internal */ + serverApi?: ServerApi; /** @event */ static readonly SERVER_OPENING = 'serverOpening' as const; @@ -202,9 +204,15 @@ export class Topology extends EventEmitter { /** * @param seedlist - a list of HostAddress instances to connect to */ - constructor(seeds: string | string[] | HostAddress | HostAddress[], options: TopologyOptions) { + constructor( + seeds: string | string[] | HostAddress | HostAddress[], + options: TopologyOptions, + serverApi?: ServerApi + ) { super(); + this.serverApi = serverApi; + // Legacy CSFLE support this.bson = Object.create(null); this.bson.serialize = serialize; diff --git a/src/utils.ts b/src/utils.ts index d80f77c9fdc..6d2f260c312 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1374,3 +1374,10 @@ export const DEFAULT_PK_FACTORY = { return new ObjectId(); } }; + +export function applyServerApiVersion(finalCmd: Document, serverApi: Document): void { + const { version, strict, deprecationErrors } = serverApi; + finalCmd.apiVersion = version; + if (strict != null) finalCmd.apiStrict = strict; + if (deprecationErrors != null) finalCmd.apiDeprecationErrors = deprecationErrors; +} diff --git a/test/functional/unified-spec-runner/entities.ts b/test/functional/unified-spec-runner/entities.ts index 32ee805b44a..08911a16ac3 100644 --- a/test/functional/unified-spec-runner/entities.ts +++ b/test/functional/unified-spec-runner/entities.ts @@ -8,7 +8,7 @@ import type { CommandSucceededEvent } from '../../../src/cmap/events'; import { patchCollectionOptions, patchDbOptions } from './unified-utils'; -import { TestConfiguration } from './unified.test'; +import { TestConfiguration } from './runner'; import { expect } from 'chai'; interface UnifiedChangeStream extends ChangeStream { @@ -30,7 +30,7 @@ export class UnifiedMongoClient extends MongoClient { } as const; constructor(url: string, description: ClientEntity) { - super(url, { monitorCommands: true, ...description.uriOptions }); + super(url, { monitorCommands: true, ...description.uriOptions }, description.serverApi); this.events = []; this.failPoints = []; this.ignoredEvents = [ diff --git a/test/functional/unified-spec-runner/match.ts b/test/functional/unified-spec-runner/match.ts index 9735063ac88..fd29fa33de4 100644 --- a/test/functional/unified-spec-runner/match.ts +++ b/test/functional/unified-spec-runner/match.ts @@ -145,8 +145,8 @@ export function resultCheck( let ok = true; const expectedEntries = Object.entries(expected); - if (depth > 1 && Object.keys(actual).length !== Object.keys(expected).length) { - throw new Error(`[${Object.keys(actual)}] length !== [${Object.keys(expected)}]`); + if (depth > 1 && Object.keys(actual).length < Object.keys(expected).length) { + throw new Error(`[${Object.keys(actual)}] length < [${Object.keys(expected)}]`); } for (const [key, value] of expectedEntries) { diff --git a/test/functional/unified-spec-runner/runner.ts b/test/functional/unified-spec-runner/runner.ts new file mode 100644 index 00000000000..46fba53e147 --- /dev/null +++ b/test/functional/unified-spec-runner/runner.ts @@ -0,0 +1,142 @@ +import { expect } from 'chai'; +import { satisfies as semverSatisfies } from 'semver'; +import * as uni from './schema'; +import { ReadPreference } from '../../../src/read_preference'; +import { zip, topologySatisfies, patchVersion } from './unified-utils'; +import { CommandEvent, EntitiesMap } from './entities'; +import { ns } from '../../../src/utils'; +import { executeOperationAndCheck } from './operations'; +import { matchesEvents } from './match'; + +export type TestConfiguration = InstanceType< + typeof import('../../tools/runner/config')['TestConfiguration'] +>; +interface MongoDBMochaTestContext extends Mocha.Context { + configuration: TestConfiguration; +} + +export async function runUnifiedTest( + ctx: MongoDBMochaTestContext, + unifiedSuite: uni.UnifiedSuite, + test: uni.Test +): Promise { + // Some basic expectations we can catch early + expect(test).to.exist; + expect(unifiedSuite).to.exist; + expect(ctx).to.exist; + expect(ctx.configuration).to.exist; + + const schemaVersion = patchVersion(unifiedSuite.schemaVersion); + expect(semverSatisfies(schemaVersion, uni.SupportedVersion)).to.be.true; + + // If test.skipReason is specified, the test runner MUST skip this + // test and MAY use the string value to log a message. + if (test.skipReason) { + console.warn(`Skipping test ${test.description}: ${test.skipReason}.`); + ctx.skip(); + } + + const UTIL_CLIENT = ctx.configuration.newClient(); + await UTIL_CLIENT.connect(); + ctx.defer(async () => await UTIL_CLIENT.close()); + + // Must fetch parameters before checking runOnRequirements + ctx.configuration.parameters = await UTIL_CLIENT.db().admin().command({ getParameter: '*' }); + + // If test.runOnRequirements is specified, the test runner MUST skip the test unless one or more + // runOnRequirement objects are satisfied. + const allRequirements = [ + ...(unifiedSuite.runOnRequirements ?? []), + ...(test.runOnRequirements ?? []) + ]; + for (const requirement of allRequirements) { + if (!topologySatisfies(ctx.configuration, requirement)) { + ctx.skip(); + } + } + + // If initialData is specified, for each collectionData therein the test runner MUST drop the + // collection and insert the specified documents (if any) using a "majority" write concern. If no + // documents are specified, the test runner MUST create the collection with a "majority" write concern. + // The test runner MUST use the internal MongoClient for these operations. + if (unifiedSuite.initialData) { + for (const collData of unifiedSuite.initialData) { + const db = UTIL_CLIENT.db(collData.databaseName); + const collection = db.collection(collData.collectionName, { + writeConcern: { w: 'majority' } + }); + const collectionList = await db.listCollections({ name: collData.collectionName }).toArray(); + if (collectionList.length !== 0) { + expect(await collection.drop()).to.be.true; + } + + if (collData.documents.length === 0) { + await db.createCollection(collData.collectionName, { + writeConcern: { w: 'majority' } + }); + continue; + } + + await collection.insertMany(collData.documents); + } + } + + const entities = await EntitiesMap.createEntities(ctx.configuration, unifiedSuite.createEntities); + ctx.defer(async () => await entities.cleanup()); + + // Workaround for SERVER-39704: + // test runners MUST execute a non-transactional distinct command on + // each mongos server before running any test that might execute distinct within a transaction. + // To ease the implementation, test runners MAY execute distinct before every test. + if ( + ctx.topologyType === uni.TopologyType.sharded || + ctx.topologyType === uni.TopologyType.shardedReplicaset + ) { + for (const [, collection] of entities.mapOf('collection')) { + await UTIL_CLIENT.db(ns(collection.namespace).db).command({ + distinct: collection.collectionName, + key: '_id' + }); + } + } + + for (const operation of test.operations) { + await executeOperationAndCheck(operation, entities); + } + + const clientEvents = new Map(); + // If any event listeners were enabled on any client entities, + // the test runner MUST now disable those event listeners. + for (const [id, client] of entities.mapOf('client')) { + clientEvents.set(id, client.stopCapturingEvents()); + } + + if (test.expectEvents) { + for (const expectedEventList of test.expectEvents) { + const clientId = expectedEventList.client; + const actualEvents = clientEvents.get(clientId); + + expect(actualEvents, `No client entity found with id ${clientId}`).to.exist; + matchesEvents(expectedEventList.events, actualEvents, entities); + } + } + + if (test.outcome) { + for (const collectionData of test.outcome) { + const collection = UTIL_CLIENT.db(collectionData.databaseName).collection( + collectionData.collectionName + ); + const findOpts = { + readConcern: 'local' as const, + readPreference: ReadPreference.primary, + sort: { _id: 'asc' as const } + }; + const documents = await collection.find({}, findOpts).toArray(); + + expect(documents).to.have.lengthOf(collectionData.documents.length); + for (const [expected, actual] of zip(collectionData.documents, documents)) { + expect(actual).to.include(expected, 'Test outcome did not match expected'); + } + } + } +} diff --git a/test/functional/unified-spec-runner/unified-utils.ts b/test/functional/unified-spec-runner/unified-utils.ts index 6abb2148bb9..14bfa85629c 100644 --- a/test/functional/unified-spec-runner/unified-utils.ts +++ b/test/functional/unified-spec-runner/unified-utils.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import type { CollectionOrDatabaseOptions, RunOnRequirement } from './schema'; -import type { TestConfiguration } from './unified.test'; +import type { TestConfiguration } from './runner'; import { gte as semverGte, lte as semverLte } from 'semver'; import { CollectionOptions, DbOptions } from '../../../src'; import { isDeepStrictEqual } from 'util'; diff --git a/test/functional/unified-spec-runner/unified.test.ts b/test/functional/unified-spec-runner/unified.test.ts index 2dc973d2105..08aea0cbca2 100644 --- a/test/functional/unified-spec-runner/unified.test.ts +++ b/test/functional/unified-spec-runner/unified.test.ts @@ -1,154 +1,19 @@ -import { expect } from 'chai'; -import { ReadPreference } from '../../../src/read_preference'; import { loadSpecTests } from '../../spec/index'; -import * as uni from './schema'; -import { patchVersion, zip, log, topologySatisfies } from './unified-utils'; -import { CommandEvent, EntitiesMap } from './entities'; -import { ns } from '../../../src/utils'; -import { executeOperationAndCheck } from './operations'; -import { satisfies as semverSatisfies } from 'semver'; -import { matchesEvents } from './match'; +import { log } from './unified-utils'; +import { runUnifiedTest, TestConfiguration } from './runner'; -export type TestConfiguration = InstanceType< - typeof import('../../tools/runner/config')['TestConfiguration'] ->; interface MongoDBMochaTestContext extends Mocha.Context { configuration: TestConfiguration; } -async function runOne( - ctx: MongoDBMochaTestContext, - unifiedSuite: uni.UnifiedSuite, - test: uni.Test -) { - // Some basic expectations we can catch early - expect(test).to.exist; - expect(unifiedSuite).to.exist; - expect(ctx).to.exist; - expect(ctx.configuration).to.exist; - - // If test.skipReason is specified, the test runner MUST skip this - // test and MAY use the string value to log a message. - if (test.skipReason) { - console.warn(`Skipping test ${test.description}: ${test.skipReason}.`); - ctx.skip(); - } - - const UTIL_CLIENT = ctx.configuration.newClient(); - await UTIL_CLIENT.connect(); - ctx.defer(async () => await UTIL_CLIENT.close()); - - // Must fetch parameters before checking runOnRequirements - ctx.configuration.parameters = await UTIL_CLIENT.db().admin().command({ getParameter: '*' }); - - // If test.runOnRequirements is specified, the test runner MUST skip the test unless one or more - // runOnRequirement objects are satisfied. - const allRequirements = [ - ...(unifiedSuite.runOnRequirements ?? []), - ...(test.runOnRequirements ?? []) - ]; - for (const requirement of allRequirements) { - if (!topologySatisfies(ctx.configuration, requirement)) { - ctx.skip(); - } - } - - // If initialData is specified, for each collectionData therein the test runner MUST drop the - // collection and insert the specified documents (if any) using a "majority" write concern. If no - // documents are specified, the test runner MUST create the collection with a "majority" write concern. - // The test runner MUST use the internal MongoClient for these operations. - if (unifiedSuite.initialData) { - for (const collData of unifiedSuite.initialData) { - const db = UTIL_CLIENT.db(collData.databaseName); - const collection = db.collection(collData.collectionName, { - writeConcern: { w: 'majority' } - }); - const collectionList = await db.listCollections({ name: collData.collectionName }).toArray(); - if (collectionList.length !== 0) { - expect(await collection.drop()).to.be.true; - } - - if (collData.documents.length === 0) { - await db.createCollection(collData.collectionName, { - writeConcern: { w: 'majority' } - }); - continue; - } - - await collection.insertMany(collData.documents); - } - } - - const entities = await EntitiesMap.createEntities(ctx.configuration, unifiedSuite.createEntities); - ctx.defer(async () => await entities.cleanup()); - - // Workaround for SERVER-39704: - // test runners MUST execute a non-transactional distinct command on - // each mongos server before running any test that might execute distinct within a transaction. - // To ease the implementation, test runners MAY execute distinct before every test. - if ( - ctx.topologyType === uni.TopologyType.sharded || - ctx.topologyType === uni.TopologyType.shardedReplicaset - ) { - for (const [, collection] of entities.mapOf('collection')) { - await UTIL_CLIENT.db(ns(collection.namespace).db).command({ - distinct: collection.collectionName, - key: '_id' - }); - } - } - - for (const operation of test.operations) { - await executeOperationAndCheck(operation, entities); - } - - const clientEvents = new Map(); - // If any event listeners were enabled on any client entities, - // the test runner MUST now disable those event listeners. - for (const [id, client] of entities.mapOf('client')) { - clientEvents.set(id, client.stopCapturingEvents()); - } - - if (test.expectEvents) { - for (const expectedEventList of test.expectEvents) { - const clientId = expectedEventList.client; - const actualEvents = clientEvents.get(clientId); - - expect(actualEvents, `No client entity found with id ${clientId}`).to.exist; - matchesEvents(expectedEventList.events, actualEvents, entities); - } - } - - if (test.outcome) { - for (const collectionData of test.outcome) { - const collection = UTIL_CLIENT.db(collectionData.databaseName).collection( - collectionData.collectionName - ); - const findOpts = { - readConcern: 'local' as const, - readPreference: ReadPreference.primary, - sort: { _id: 'asc' as const } - }; - const documents = await collection.find({}, findOpts).toArray(); - - expect(documents).to.have.lengthOf(collectionData.documents.length); - for (const [expected, actual] of zip(collectionData.documents, documents)) { - expect(actual).to.include(expected, 'Test outcome did not match expected'); - } - } - } -} - describe('Unified test format', function unifiedTestRunner() { // Valid tests that should pass for (const unifiedSuite of loadSpecTests('unified-test-format/valid-pass')) { - const schemaVersion = patchVersion(unifiedSuite.schemaVersion); - expect(semverSatisfies(schemaVersion, uni.SupportedVersion)).to.be.true; - context(String(unifiedSuite.description), function runUnifiedTest() { + context(String(unifiedSuite.description), function runUnifiedTestSuite() { for (const test of unifiedSuite.tests) { it(String(test.description), async function runOneUnifiedTest() { try { - await runOne(this as MongoDBMochaTestContext, unifiedSuite, test); + await runUnifiedTest(this as MongoDBMochaTestContext, unifiedSuite, test); } catch (error) { if (error.message.includes('not implemented.')) { log(`${test.description}: was skipped due to missing functionality`); diff --git a/test/functional/versioned-api.test.js b/test/functional/versioned-api.test.js new file mode 100644 index 00000000000..6372e5b84be --- /dev/null +++ b/test/functional/versioned-api.test.js @@ -0,0 +1,28 @@ +'use strict'; + +const { expect } = require('chai'); +const { loadSpecTests } = require('../spec/index'); +const { runUnifiedTest } = require('./unified-spec-runner/runner'); + +describe('Versioned API', function () { + for (const versionedApiTest of loadSpecTests('versioned-api')) { + expect(versionedApiTest).to.exist; + context(String(versionedApiTest.description), function () { + for (const test of versionedApiTest.tests) { + it(String(test.description), async function () { + try { + await runUnifiedTest(this, versionedApiTest, test); + } catch (error) { + if (error.message.includes('not implemented.')) { + console.log(`${test.description}: was skipped due to missing functionality`); + console.log(error.stack); + this.skip(); + } else { + throw error; + } + } + }); + } + }); + } +}); diff --git a/test/spec/versioned-api/crud-api-version-1-strict.json b/test/spec/versioned-api/crud-api-version-1-strict.json new file mode 100644 index 00000000000..a2eb02e4321 --- /dev/null +++ b/test/spec/versioned-api/crud-api-version-1-strict.json @@ -0,0 +1,1076 @@ +{ + "description": "CRUD Api Version 1 (strict)", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "4.7" + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "commandStartedEvent" + ], + "serverApi": { + "version": "1", + "strict": true + } + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "versioned-api-tests" + } + }, + { + "database": { + "id": "adminDatabase", + "client": "client", + "databaseName": "admin" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "test" + } + } + ], + "_yamlAnchors": { + "versions": [ + { + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + ] + }, + "initialData": [ + { + "collectionName": "test", + "databaseName": "versioned-api-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "tests": [ + { + "description": "aggregate on collection appends declared API version", + "operations": [ + { + "name": "aggregate", + "object": "collection", + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "test", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "aggregate on database appends declared API version", + "operations": [ + { + "name": "aggregate", + "object": "adminDatabase", + "arguments": { + "pipeline": [ + { + "$listLocalSessions": {} + }, + { + "$limit": 1 + } + ] + }, + "expectError": { + "errorCodeName": "APIStrictError" + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": 1, + "pipeline": [ + { + "$listLocalSessions": {} + }, + { + "$limit": 1 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "bulkWrite appends declared API version", + "operations": [ + { + "name": "bulkWrite", + "object": "collection", + "arguments": { + "requests": [ + { + "insertOne": { + "document": { + "_id": 6, + "x": 66 + } + } + }, + { + "updateOne": { + "filter": { + "_id": 2 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "deleteMany": { + "filter": { + "x": { + "$nin": [ + 24, + 34 + ] + } + } + } + }, + { + "updateMany": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "deleteOne": { + "filter": { + "_id": 7 + } + } + }, + { + "replaceOne": { + "filter": { + "_id": 4 + }, + "replacement": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + } + ], + "ordered": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 2 + }, + "u": { + "$inc": { + "x": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "x": { + "$nin": [ + 24, + 34 + ] + } + }, + "limit": 0 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 7 + }, + "limit": 1 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 4 + }, + "u": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "countDocuments appends declared API version", + "operations": [ + { + "name": "countDocuments", + "object": "collection", + "arguments": { + "filter": { + "x": { + "$gt": 11 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "test", + "pipeline": [ + { + "$match": { + "x": { + "$gt": 11 + } + } + }, + { + "$group": { + "_id": 1, + "n": { + "$sum": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "deleteMany appends declared API version", + "operations": [ + { + "name": "deleteMany", + "object": "collection", + "arguments": { + "filter": { + "x": { + "$nin": [ + 24, + 34 + ] + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "x": { + "$nin": [ + 24, + 34 + ] + } + }, + "limit": 0 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "deleteOne appends declared API version", + "operations": [ + { + "name": "deleteOne", + "object": "collection", + "arguments": { + "filter": { + "_id": 7 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 7 + }, + "limit": 1 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "distinct appends declared API version", + "operations": [ + { + "name": "distinct", + "object": "collection", + "arguments": { + "fieldName": "x", + "filter": {} + }, + "expectError": { + "isError": true, + "errorContains": "command distinct is not in API Version 1", + "errorCodeName": "APIStrictError" + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "distinct": "test", + "key": "x", + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "estimatedDocumentCount appends declared API version", + "skipReason": "DRIVERS-1437 count was removed from API version 1", + "operations": [ + { + "name": "estimatedDocumentCount", + "object": "collection", + "arguments": {} + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "count": "test", + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "find command with declared API version appends to the command, but getMore does not", + "operations": [ + { + "name": "find", + "object": "collection", + "arguments": { + "filter": {}, + "sort": { + "_id": 1 + }, + "batchSize": 3 + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "test", + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "apiVersion": { + "$$exists": false + }, + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "findOneAndDelete appends declared API version", + "operations": [ + { + "name": "findOneAndDelete", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "test", + "query": { + "_id": 1 + }, + "remove": true, + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "findOneAndReplace appends declared API version", + "operations": [ + { + "name": "findOneAndReplace", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "x": 33 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "test", + "query": { + "_id": 1 + }, + "update": { + "x": 33 + }, + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "findOneAndUpdate appends declared API version", + "operations": [ + { + "name": "findOneAndUpdate", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "test", + "query": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "insertMany appends declared API version", + "operations": [ + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 6, + "x": 66 + }, + { + "_id": 7, + "x": 77 + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + }, + { + "_id": 7, + "x": 77 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "insertOne appends declared API version", + "operations": [ + { + "name": "insertOne", + "object": "collection", + "arguments": { + "document": { + "_id": 6, + "x": 66 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "replaceOne appends declared API version", + "operations": [ + { + "name": "replaceOne", + "object": "collection", + "arguments": { + "filter": { + "_id": 4 + }, + "replacement": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 4 + }, + "u": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "updateMany appends declared API version", + "operations": [ + { + "name": "updateMany", + "object": "collection", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "updateOne appends declared API version", + "operations": [ + { + "name": "updateOne", + "object": "collection", + "arguments": { + "filter": { + "_id": 2 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 2 + }, + "u": { + "$inc": { + "x": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/versioned-api/crud-api-version-1-strict.yml b/test/spec/versioned-api/crud-api-version-1-strict.yml new file mode 100644 index 00000000000..48aa14540e7 --- /dev/null +++ b/test/spec/versioned-api/crud-api-version-1-strict.yml @@ -0,0 +1,395 @@ +description: "CRUD Api Version 1 (strict)" + +schemaVersion: "1.1" + +runOnRequirements: + - minServerVersion: "4.7" + +createEntities: + - client: + id: &client client + observeEvents: + - commandStartedEvent + serverApi: + version: "1" + strict: true + - database: + id: &database database + client: *client + databaseName: &databaseName versioned-api-tests + - database: + id: &adminDatabase adminDatabase + client: *client + databaseName: &adminDatabaseName admin + - collection: + id: &collection collection + database: *database + collectionName: &collectionName test + +_yamlAnchors: + versions: + - &expectedApiVersion + apiVersion: "1" + apiStrict: true + apiDeprecationErrors: { $$unsetOrMatches: false } + +initialData: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - { _id: 4, x: 44 } + - { _id: 5, x: 55 } + +tests: + - description: "aggregate on collection appends declared API version" + operations: + - name: aggregate + object: *collection + arguments: + pipeline: &pipeline + - $sort: { x : 1 } + - $match: { _id: { $gt: 1 } } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + aggregate: *collectionName + pipeline: *pipeline + <<: *expectedApiVersion + + - description: "aggregate on database appends declared API version" + operations: + - name: aggregate + object: *adminDatabase + arguments: + pipeline: &pipeline + - $listLocalSessions: {} + - $limit: 1 + expectError: + errorCodeName: "APIStrictError" + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + aggregate: 1 + pipeline: *pipeline + <<: *expectedApiVersion + + - description: "bulkWrite appends declared API version" + operations: + - name: bulkWrite + object: *collection + arguments: + requests: + - insertOne: + document: { _id: 6, x: 66 } + - updateOne: + filter: { _id: 2 } + update: { $inc: { x: 1 } } + - deleteMany: + filter: { x: { $nin: [ 24, 34 ] } } + - updateMany: + filter: { _id: { $gt: 1 } } + update: { $inc: { x: 1 } } + - deleteOne: + filter: { _id: 7 } + - replaceOne: + filter: { _id: 4 } + replacement: { _id: 4, x: 44 } + upsert: true + ordered: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + insert: *collectionName + documents: + - { _id: 6, x: 66 } + <<: *expectedApiVersion + - commandStartedEvent: + command: + update: *collectionName + updates: + - { q: { _id: 2 }, u: { $inc: { x: 1 } } } + <<: *expectedApiVersion + - commandStartedEvent: + command: + delete: *collectionName + deletes: + - { q: { x: { $nin: [ 24, 34 ] } }, limit: 0 } + <<: *expectedApiVersion + - commandStartedEvent: + command: + update: *collectionName + updates: + - { q: { _id: { $gt: 1 } }, u: { $inc: { x: 1 } }, multi: true } + <<: *expectedApiVersion + - commandStartedEvent: + command: + delete: *collectionName + deletes: + - { q: { _id: 7 }, limit: 1 } + <<: *expectedApiVersion + - commandStartedEvent: + command: + update: *collectionName + updates: + - { q: { _id: 4 }, u: { _id: 4, x: 44 }, upsert: true } + <<: *expectedApiVersion + + - description: "countDocuments appends declared API version" + operations: + - name: countDocuments + object: *collection + arguments: + filter: &filter + x : { $gt: 11 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + aggregate: *collectionName + pipeline: + - { $match: *filter } + - { $group: { _id: 1, n: { $sum: 1 } } } + <<: *expectedApiVersion + + - description: "deleteMany appends declared API version" + operations: + - name: deleteMany + object: *collection + arguments: + filter: { x: { $nin: [ 24, 34 ] } } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + delete: *collectionName + deletes: + - { q: { x: { $nin: [ 24, 34 ] } }, limit: 0 } + <<: *expectedApiVersion + + - description: "deleteOne appends declared API version" + operations: + - name: deleteOne + object: *collection + arguments: + filter: { _id: 7 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + delete: *collectionName + deletes: + - { q: { _id: 7 }, limit: 1 } + <<: *expectedApiVersion + + # distinct will fail until drivers replace it with an alternative + # implementation + - description: "distinct appends declared API version" + operations: + - name: distinct + object: *collection + arguments: + fieldName: x + filter: {} + expectError: + isError: true + errorContains: "command distinct is not in API Version 1" + errorCodeName: "APIStrictError" + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + distinct: *collectionName + key: x + <<: *expectedApiVersion + + - description: "estimatedDocumentCount appends declared API version" + skipReason: "DRIVERS-1437 count was removed from API version 1" + operations: + - name: estimatedDocumentCount + object: *collection + arguments: {} + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + count: *collectionName + <<: *expectedApiVersion + + - description: "find command with declared API version appends to the command, but getMore does not" + operations: + - name: find + object: *collection + arguments: + filter: {} + sort: { _id: 1 } + batchSize: 3 + expectResult: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - { _id: 4, x: 44 } + - { _id: 5, x: 55 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + find: *collectionName + <<: *expectedApiVersion + - commandStartedEvent: + command: + getMore: { $$type: [ int, long ] } + apiVersion: { $$exists: false } + apiStrict: { $$exists: false } + apiDeprecationErrors: { $$exists: false } + + - description: "findOneAndDelete appends declared API version" + operations: + - name: findOneAndDelete + object: *collection + arguments: + filter: &filter { _id: 1 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + findAndModify: *collectionName + query: *filter + remove: true + <<: *expectedApiVersion + + - description: "findOneAndReplace appends declared API version" + operations: + - name: findOneAndReplace + object: *collection + arguments: + filter: &filter { _id: 1 } + replacement: &replacement { x: 33 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + findAndModify: *collectionName + query: *filter + update: *replacement + <<: *expectedApiVersion + + - description: "findOneAndUpdate appends declared API version" + operations: + - name: findOneAndUpdate + object: collection + arguments: + filter: &filter { _id: 1 } + update: &update { $inc: { x: 1 } } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + findAndModify: *collectionName + query: *filter + update: *update + <<: *expectedApiVersion + + - description: "insertMany appends declared API version" + operations: + - name: insertMany + object: *collection + arguments: + documents: + - { _id: 6, x: 66 } + - { _id: 7, x: 77 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + insert: *collectionName + documents: + - { _id: 6, x: 66 } + - { _id: 7, x: 77 } + <<: *expectedApiVersion + + - description: "insertOne appends declared API version" + operations: + - name: insertOne + object: *collection + arguments: + document: { _id: 6, x: 66 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + insert: *collectionName + documents: + - { _id: 6, x: 66 } + <<: *expectedApiVersion + + - description: "replaceOne appends declared API version" + operations: + - name: replaceOne + object: *collection + arguments: + filter: { _id: 4 } + replacement: { _id: 4, x: 44 } + upsert: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + update: *collectionName + updates: + - { q: { _id: 4 }, u: { _id: 4, x: 44 }, upsert: true } + <<: *expectedApiVersion + + - description: "updateMany appends declared API version" + operations: + - name: updateMany + object: *collection + arguments: + filter: { _id: { $gt: 1 } } + update: { $inc: { x: 1 } } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + update: *collectionName + updates: + - { q: { _id: { $gt: 1 } }, u: { $inc: { x: 1 } }, multi: true } + <<: *expectedApiVersion + + - description: "updateOne appends declared API version" + operations: + - name: updateOne + object: *collection + arguments: + filter: { _id: 2 } + update: { $inc: { x: 1 } } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + update: *collectionName + updates: + - { q: { _id: 2 }, u: { $inc: { x: 1 } } } + <<: *expectedApiVersion diff --git a/test/spec/versioned-api/crud-api-version-1.json b/test/spec/versioned-api/crud-api-version-1.json new file mode 100644 index 00000000000..6584d8d2aea --- /dev/null +++ b/test/spec/versioned-api/crud-api-version-1.json @@ -0,0 +1,1067 @@ +{ + "description": "CRUD Api Version 1", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "4.7" + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "commandStartedEvent" + ], + "serverApi": { + "version": "1", + "deprecationErrors": true + } + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "versioned-api-tests" + } + }, + { + "database": { + "id": "adminDatabase", + "client": "client", + "databaseName": "admin" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "test" + } + } + ], + "_yamlAnchors": { + "versions": [ + { + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + ] + }, + "initialData": [ + { + "collectionName": "test", + "databaseName": "versioned-api-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "tests": [ + { + "description": "aggregate on collection appends declared API version", + "operations": [ + { + "name": "aggregate", + "object": "collection", + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "test", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "aggregate on database appends declared API version", + "operations": [ + { + "name": "aggregate", + "object": "adminDatabase", + "arguments": { + "pipeline": [ + { + "$listLocalSessions": {} + }, + { + "$limit": 1 + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": 1, + "pipeline": [ + { + "$listLocalSessions": {} + }, + { + "$limit": 1 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "bulkWrite appends declared API version", + "operations": [ + { + "name": "bulkWrite", + "object": "collection", + "arguments": { + "requests": [ + { + "insertOne": { + "document": { + "_id": 6, + "x": 66 + } + } + }, + { + "updateOne": { + "filter": { + "_id": 2 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "deleteMany": { + "filter": { + "x": { + "$nin": [ + 24, + 34 + ] + } + } + } + }, + { + "updateMany": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "deleteOne": { + "filter": { + "_id": 7 + } + } + }, + { + "replaceOne": { + "filter": { + "_id": 4 + }, + "replacement": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + } + ], + "ordered": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + }, + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 2 + }, + "u": { + "$inc": { + "x": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + }, + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "x": { + "$nin": [ + 24, + 34 + ] + } + }, + "limit": 0 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + }, + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + }, + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 7 + }, + "limit": 1 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + }, + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 4 + }, + "u": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "countDocuments appends declared API version", + "operations": [ + { + "name": "countDocuments", + "object": "collection", + "arguments": { + "filter": { + "x": { + "$gt": 11 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "test", + "pipeline": [ + { + "$match": { + "x": { + "$gt": 11 + } + } + }, + { + "$group": { + "_id": 1, + "n": { + "$sum": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "deleteMany appends declared API version", + "operations": [ + { + "name": "deleteMany", + "object": "collection", + "arguments": { + "filter": { + "x": { + "$nin": [ + 24, + 34 + ] + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "x": { + "$nin": [ + 24, + 34 + ] + } + }, + "limit": 0 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "deleteOne appends declared API version", + "operations": [ + { + "name": "deleteOne", + "object": "collection", + "arguments": { + "filter": { + "_id": 7 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 7 + }, + "limit": 1 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "distinct appends declared API version", + "operations": [ + { + "name": "distinct", + "object": "collection", + "arguments": { + "fieldName": "x", + "filter": {} + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "distinct": "test", + "key": "x", + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "estimatedDocumentCount appends declared API version", + "operations": [ + { + "name": "estimatedDocumentCount", + "object": "collection", + "arguments": {} + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "count": "test", + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "find command with declared API version appends to the command, but getMore does not", + "operations": [ + { + "name": "find", + "object": "collection", + "arguments": { + "filter": {}, + "sort": { + "_id": 1 + }, + "batchSize": 3 + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "test", + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "apiVersion": { + "$$exists": false + }, + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "findOneAndDelete appends declared API version", + "operations": [ + { + "name": "findOneAndDelete", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "test", + "query": { + "_id": 1 + }, + "remove": true, + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "findOneAndReplace appends declared API version", + "operations": [ + { + "name": "findOneAndReplace", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "x": 33 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "test", + "query": { + "_id": 1 + }, + "update": { + "x": 33 + }, + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "findOneAndUpdate appends declared API version", + "operations": [ + { + "name": "findOneAndUpdate", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "test", + "query": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "insertMany appends declared API version", + "operations": [ + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 6, + "x": 66 + }, + { + "_id": 7, + "x": 77 + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + }, + { + "_id": 7, + "x": 77 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "insertOne appends declared API version", + "operations": [ + { + "name": "insertOne", + "object": "collection", + "arguments": { + "document": { + "_id": 6, + "x": 66 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "replaceOne appends declared API version", + "operations": [ + { + "name": "replaceOne", + "object": "collection", + "arguments": { + "filter": { + "_id": 4 + }, + "replacement": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 4 + }, + "u": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "updateMany appends declared API version", + "operations": [ + { + "name": "updateMany", + "object": "collection", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "updateOne appends declared API version", + "operations": [ + { + "name": "updateOne", + "object": "collection", + "arguments": { + "filter": { + "_id": 2 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 2 + }, + "u": { + "$inc": { + "x": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/versioned-api/crud-api-version-1.yml b/test/spec/versioned-api/crud-api-version-1.yml new file mode 100644 index 00000000000..250f29ba40f --- /dev/null +++ b/test/spec/versioned-api/crud-api-version-1.yml @@ -0,0 +1,388 @@ +description: "CRUD Api Version 1" + +schemaVersion: "1.1" + +runOnRequirements: + - minServerVersion: "4.7" + +createEntities: + - client: + id: &client client + observeEvents: + - commandStartedEvent + serverApi: + version: "1" + # Deprecation errors is set to true to ensure that drivers don't use any + # deprecated server API in their logic. + deprecationErrors: true + - database: + id: &database database + client: *client + databaseName: &databaseName versioned-api-tests + - database: + id: &adminDatabase adminDatabase + client: *client + databaseName: &adminDatabaseName admin + - collection: + id: &collection collection + database: *database + collectionName: &collectionName test + +_yamlAnchors: + versions: + - &expectedApiVersion + apiVersion: "1" + apiStrict: { $$unsetOrMatches: false } + apiDeprecationErrors: true + +initialData: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - { _id: 4, x: 44 } + - { _id: 5, x: 55 } + +tests: + - description: "aggregate on collection appends declared API version" + operations: + - name: aggregate + object: *collection + arguments: + pipeline: &pipeline + - $sort: { x : 1 } + - $match: { _id: { $gt: 1 } } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + aggregate: *collectionName + pipeline: *pipeline + <<: *expectedApiVersion + + - description: "aggregate on database appends declared API version" + operations: + - name: aggregate + object: *adminDatabase + arguments: + pipeline: &pipeline + - $listLocalSessions: {} + - $limit: 1 + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + aggregate: 1 + pipeline: *pipeline + <<: *expectedApiVersion + + - description: "bulkWrite appends declared API version" + operations: + - name: bulkWrite + object: *collection + arguments: + requests: + - insertOne: + document: { _id: 6, x: 66 } + - updateOne: + filter: { _id: 2 } + update: { $inc: { x: 1 } } + - deleteMany: + filter: { x: { $nin: [ 24, 34 ] } } + - updateMany: + filter: { _id: { $gt: 1 } } + update: { $inc: { x: 1 } } + - deleteOne: + filter: { _id: 7 } + - replaceOne: + filter: { _id: 4 } + replacement: { _id: 4, x: 44 } + upsert: true + ordered: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + insert: *collectionName + documents: + - { _id: 6, x: 66 } + <<: *expectedApiVersion + - commandStartedEvent: + command: + update: *collectionName + updates: + - { q: { _id: 2 }, u: { $inc: { x: 1 } } } + <<: *expectedApiVersion + - commandStartedEvent: + command: + delete: *collectionName + deletes: + - { q: { x: { $nin: [ 24, 34 ] } }, limit: 0 } + <<: *expectedApiVersion + - commandStartedEvent: + command: + update: *collectionName + updates: + - { q: { _id: { $gt: 1 } }, u: { $inc: { x: 1 } }, multi: true } + <<: *expectedApiVersion + - commandStartedEvent: + command: + delete: *collectionName + deletes: + - { q: { _id: 7 }, limit: 1 } + <<: *expectedApiVersion + - commandStartedEvent: + command: + update: *collectionName + updates: + - { q: { _id: 4 }, u: { _id: 4, x: 44 }, upsert: true } + <<: *expectedApiVersion + + - description: "countDocuments appends declared API version" + operations: + - name: countDocuments + object: *collection + arguments: + filter: &filter + x : { $gt: 11 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + aggregate: *collectionName + pipeline: + - { $match: *filter } + - { $group: { _id: 1, n: { $sum: 1 } } } + <<: *expectedApiVersion + + - description: "deleteMany appends declared API version" + operations: + - name: deleteMany + object: *collection + arguments: + filter: { x: { $nin: [ 24, 34 ] } } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + delete: *collectionName + deletes: + - { q: { x: { $nin: [ 24, 34 ] } }, limit: 0 } + <<: *expectedApiVersion + + - description: "deleteOne appends declared API version" + operations: + - name: deleteOne + object: *collection + arguments: + filter: { _id: 7 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + delete: *collectionName + deletes: + - { q: { _id: 7 }, limit: 1 } + <<: *expectedApiVersion + + - description: "distinct appends declared API version" + operations: + - name: distinct + object: *collection + arguments: + fieldName: x + filter: {} + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + distinct: *collectionName + key: x + <<: *expectedApiVersion + + - description: "estimatedDocumentCount appends declared API version" + operations: + - name: estimatedDocumentCount + object: *collection + arguments: {} + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + count: *collectionName + <<: *expectedApiVersion + + - description: "find command with declared API version appends to the command, but getMore does not" + operations: + - name: find + object: *collection + arguments: + filter: {} + sort: { _id: 1 } + batchSize: 3 + expectResult: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - { _id: 4, x: 44 } + - { _id: 5, x: 55 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + find: *collectionName + <<: *expectedApiVersion + - commandStartedEvent: + command: + getMore: { $$type: [ int, long ] } + apiVersion: { $$exists: false } + apiStrict: { $$exists: false } + apiDeprecationErrors: { $$exists: false } + + - description: "findOneAndDelete appends declared API version" + operations: + - name: findOneAndDelete + object: *collection + arguments: + filter: &filter { _id: 1 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + findAndModify: *collectionName + query: *filter + remove: true + <<: *expectedApiVersion + + - description: "findOneAndReplace appends declared API version" + operations: + - name: findOneAndReplace + object: *collection + arguments: + filter: &filter { _id: 1 } + replacement: &replacement { x: 33 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + findAndModify: *collectionName + query: *filter + update: *replacement + <<: *expectedApiVersion + + - description: "findOneAndUpdate appends declared API version" + operations: + - name: findOneAndUpdate + object: collection + arguments: + filter: &filter { _id: 1 } + update: &update { $inc: { x: 1 } } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + findAndModify: *collectionName + query: *filter + update: *update + <<: *expectedApiVersion + + - description: "insertMany appends declared API version" + operations: + - name: insertMany + object: *collection + arguments: + documents: + - { _id: 6, x: 66 } + - { _id: 7, x: 77 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + insert: *collectionName + documents: + - { _id: 6, x: 66 } + - { _id: 7, x: 77 } + <<: *expectedApiVersion + + - description: "insertOne appends declared API version" + operations: + - name: insertOne + object: *collection + arguments: + document: { _id: 6, x: 66 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + insert: *collectionName + documents: + - { _id: 6, x: 66 } + <<: *expectedApiVersion + + - description: "replaceOne appends declared API version" + operations: + - name: replaceOne + object: *collection + arguments: + filter: { _id: 4 } + replacement: { _id: 4, x: 44 } + upsert: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + update: *collectionName + updates: + - { q: { _id: 4 }, u: { _id: 4, x: 44 }, upsert: true } + <<: *expectedApiVersion + + - description: "updateMany appends declared API version" + operations: + - name: updateMany + object: *collection + arguments: + filter: { _id: { $gt: 1 } } + update: { $inc: { x: 1 } } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + update: *collectionName + updates: + - { q: { _id: { $gt: 1 } }, u: { $inc: { x: 1 } }, multi: true } + <<: *expectedApiVersion + + - description: "updateOne appends declared API version" + operations: + - name: updateOne + object: *collection + arguments: + filter: { _id: 2 } + update: { $inc: { x: 1 } } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + update: *collectionName + updates: + - { q: { _id: 2 }, u: { $inc: { x: 1 } } } + <<: *expectedApiVersion diff --git a/test/spec/versioned-api/runcommand-helper-no-api-version-declared.json b/test/spec/versioned-api/runcommand-helper-no-api-version-declared.json new file mode 100644 index 00000000000..65c24ef4604 --- /dev/null +++ b/test/spec/versioned-api/runcommand-helper-no-api-version-declared.json @@ -0,0 +1,117 @@ +{ + "description": "RunCommand helper: No API version declared", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "4.7", + "serverParameters": { + "requireApiVersion": false + } + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "versioned-api-tests" + } + } + ], + "tests": [ + { + "description": "runCommand does not inspect or change the command document", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1, + "apiVersion": "server_will_never_support_this_api_version" + } + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "ping": 1, + "apiVersion": "server_will_never_support_this_api_version", + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + }, + "commandName": "ping", + "databaseName": "versioned-api-tests" + } + } + ] + } + ] + }, + { + "description": "runCommand does not prevent sending invalid API version declarations", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1, + "apiStrict": true + } + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "ping": 1, + "apiVersion": { + "$$exists": false + }, + "apiStrict": true, + "apiDeprecationErrors": { + "$$exists": false + } + }, + "commandName": "ping", + "databaseName": "versioned-api-tests" + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/versioned-api/runcommand-helper-no-api-version-declared.yml b/test/spec/versioned-api/runcommand-helper-no-api-version-declared.yml new file mode 100644 index 00000000000..dc31897e2e0 --- /dev/null +++ b/test/spec/versioned-api/runcommand-helper-no-api-version-declared.yml @@ -0,0 +1,67 @@ +description: "RunCommand helper: No API version declared" + +schemaVersion: "1.1" + +runOnRequirements: + - minServerVersion: "4.7" + serverParameters: + requireApiVersion: false + +createEntities: + - client: + id: &client client + observeEvents: + - commandStartedEvent + - database: + id: &database database + client: *client + databaseName: &databaseName versioned-api-tests + +tests: + - description: "runCommand does not inspect or change the command document" + operations: + - name: runCommand + object: *database + arguments: + commandName: ping + command: + ping: 1 + apiVersion: "server_will_never_support_this_api_version" + expectError: + isError: true + isClientError: false + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + ping: 1 + apiVersion: "server_will_never_support_this_api_version" + apiStrict: { $$exists: false } + apiDeprecationErrors: { $$exists: false } + commandName: ping + databaseName: *databaseName + + - description: "runCommand does not prevent sending invalid API version declarations" + operations: + - name: runCommand + object: *database + arguments: + commandName: ping + command: + ping: 1 + apiStrict: true + expectError: + isError: true + isClientError: false + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + ping: 1 + apiVersion: { $$exists: false } + apiStrict: true + apiDeprecationErrors: { $$exists: false } + commandName: ping + databaseName: *databaseName diff --git a/test/spec/versioned-api/transaction-handling.json b/test/spec/versioned-api/transaction-handling.json new file mode 100644 index 00000000000..64e9706b5ef --- /dev/null +++ b/test/spec/versioned-api/transaction-handling.json @@ -0,0 +1,388 @@ +{ + "description": "Transaction handling", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "4.7", + "topologies": [ + "replicaset", + "sharded-replicaset" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "commandStartedEvent" + ], + "serverApi": { + "version": "1" + } + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "versioned-api-tests" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "test" + } + }, + { + "session": { + "id": "session", + "client": "client" + } + } + ], + "_yamlAnchors": { + "versions": [ + { + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + }, + { + "apiVersion": { + "$$exists": false + }, + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + } + ] + }, + "initialData": [ + { + "collectionName": "test", + "databaseName": "versioned-api-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "tests": [ + { + "description": "Only the first command in a transaction declares an API version", + "runOnRequirements": [ + { + "topologies": [ + "replicaset", + "sharded-replicaset" + ] + } + ], + "operations": [ + { + "name": "startTransaction", + "object": "session" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session", + "document": { + "_id": 6, + "x": 66 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 6 + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session", + "document": { + "_id": 7, + "x": 77 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 7 + } + } + } + }, + { + "name": "commitTransaction", + "object": "session" + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + } + ], + "lsid": { + "$$sessionLsid": "session" + }, + "startTransaction": true, + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 7, + "x": 77 + } + ], + "lsid": { + "$$sessionLsid": "session" + }, + "apiVersion": { + "$$exists": false + }, + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "commitTransaction": 1, + "lsid": { + "$$sessionLsid": "session" + }, + "apiVersion": { + "$$exists": false + }, + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "Committing a transaction twice does not append server API options", + "runOnRequirements": [ + { + "topologies": [ + "replicaset", + "sharded-replicaset" + ] + } + ], + "operations": [ + { + "name": "startTransaction", + "object": "session" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session", + "document": { + "_id": 6, + "x": 66 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 6 + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session", + "document": { + "_id": 7, + "x": 77 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 7 + } + } + } + }, + { + "name": "commitTransaction", + "object": "session" + }, + { + "name": "commitTransaction", + "object": "session" + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + } + ], + "lsid": { + "$$sessionLsid": "session" + }, + "startTransaction": true, + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 7, + "x": 77 + } + ], + "lsid": { + "$$sessionLsid": "session" + }, + "apiVersion": { + "$$exists": false + }, + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "commitTransaction": 1, + "lsid": { + "$$sessionLsid": "session" + }, + "apiVersion": { + "$$exists": false + }, + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "commitTransaction": 1, + "lsid": { + "$$sessionLsid": "session" + }, + "apiVersion": { + "$$exists": false + }, + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + } + } + } + ] + } + ] + } + ] +} + diff --git a/test/spec/versioned-api/transaction-handling.yml b/test/spec/versioned-api/transaction-handling.yml new file mode 100644 index 00000000000..97616f6f7a4 --- /dev/null +++ b/test/spec/versioned-api/transaction-handling.yml @@ -0,0 +1,140 @@ +description: "Transaction handling" + +schemaVersion: "1.1" + +runOnRequirements: + - minServerVersion: "4.7" + topologies: [ replicaset, sharded-replicaset ] + +createEntities: + - client: + id: &client client + observeEvents: + - commandStartedEvent + serverApi: + version: "1" + - database: + id: &database database + client: *client + databaseName: &databaseName versioned-api-tests + - collection: + id: &collection collection + database: *database + collectionName: &collectionName test + - session: + id: &session session + client: *client + +_yamlAnchors: + versions: + - &expectedApiVersion + apiVersion: "1" + apiStrict: { $$unsetOrMatches: false } + apiDeprecationErrors: { $$unsetOrMatches: false } + - &noApiVersion + apiVersion: { $$exists: false } + apiStrict: { $$exists: false } + apiDeprecationErrors: { $$exists: false } + + +initialData: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - { _id: 4, x: 44 } + - { _id: 5, x: 55 } + +tests: + - description: "Only the first command in a transaction declares an API version" + runOnRequirements: + - topologies: [ replicaset, sharded-replicaset ] + operations: + - name: startTransaction + object: *session + - name: insertOne + object: *collection + arguments: + session: *session + document: { _id: 6, x: 66 } + expectResult: { $$unsetOrMatches: { insertedId: { $$unsetOrMatches: 6 } } } + - name: insertOne + object: *collection + arguments: + session: *session + document: { _id: 7, x: 77 } + expectResult: { $$unsetOrMatches: { insertedId: { $$unsetOrMatches: 7 } } } + - name: commitTransaction + object: *session + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + insert: *collectionName + documents: [ { _id: 6, x: 66 } ] + lsid: { $$sessionLsid: *session } + startTransaction: true + <<: *expectedApiVersion + - commandStartedEvent: + command: + insert: *collectionName + documents: [ { _id: 7, x: 77 } ] + lsid: { $$sessionLsid: *session } + <<: *noApiVersion + - commandStartedEvent: + command: + commitTransaction: 1 + lsid: { $$sessionLsid: *session } + <<: *noApiVersion + - description: "Committing a transaction twice does not append server API options" + runOnRequirements: + - topologies: [ replicaset, sharded-replicaset ] + operations: + - name: startTransaction + object: *session + - name: insertOne + object: *collection + arguments: + session: *session + document: { _id: 6, x: 66 } + expectResult: { $$unsetOrMatches: { insertedId: { $$unsetOrMatches: 6 } } } + - name: insertOne + object: *collection + arguments: + session: *session + document: { _id: 7, x: 77 } + expectResult: { $$unsetOrMatches: { insertedId: { $$unsetOrMatches: 7 } } } + - name: commitTransaction + object: *session + - name: commitTransaction + object: *session + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + insert: *collectionName + documents: [ { _id: 6, x: 66 } ] + lsid: { $$sessionLsid: *session } + startTransaction: true + <<: *expectedApiVersion + - commandStartedEvent: + command: + insert: *collectionName + documents: [ { _id: 7, x: 77 } ] + lsid: { $$sessionLsid: *session } + <<: *noApiVersion + - commandStartedEvent: + command: + commitTransaction: 1 + lsid: { $$sessionLsid: *session } + <<: *noApiVersion + - commandStartedEvent: + command: + commitTransaction: 1 + lsid: { $$sessionLsid: *session } + <<: *noApiVersion + diff --git a/test/tools/runner/config.js b/test/tools/runner/config.js index ddc1ae92752..c90c150a96b 100644 --- a/test/tools/runner/config.js +++ b/test/tools/runner/config.js @@ -22,11 +22,12 @@ function convertToConnStringMap(obj) { } class TestConfiguration { - constructor(uri, context) { + constructor(uri, context, serverApiVersion) { const { url, hosts } = parseURI(uri); const hostAddresses = hosts.map(HostAddress.fromString); this.topologyType = context.topologyType; this.version = context.version; + this.serverApiVersion = serverApiVersion; this.clientSideEncryption = context.clientSideEncryption; this.parameters = undefined; this.options = { @@ -88,7 +89,8 @@ class TestConfiguration { if (typeof dbOptions === 'string') { return new MongoClient( dbOptions, - Object.assign({ minHeartbeatFrequencyMS: 100 }, serverOptions) + Object.assign({ minHeartbeatFrequencyMS: 100 }, serverOptions), + { version: this.serverApiVersion } ); } diff --git a/test/tools/runner/index.js b/test/tools/runner/index.js index a1c2929ff12..ad2fc589c4a 100644 --- a/test/tools/runner/index.js +++ b/test/tools/runner/index.js @@ -9,6 +9,7 @@ const mock = require('../mock'); const wtfnode = require('wtfnode'); const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017'; +const MONGODB_API_VERSION = process.env.MONGODB_API_VERSION; const filters = []; function initializeFilters(client, callback) { @@ -77,7 +78,7 @@ before(function (_done) { // replace this when mocha supports dynamic skipping with `afterEach` filterOutTests(this._runnable.parent); - this.configuration = new TestConfiguration(MONGODB_URI, context); + this.configuration = new TestConfiguration(MONGODB_URI, context, MONGODB_API_VERSION); done(); }); }); From 8bfc97c83a0c0127a20530480543785d0fc18d7c Mon Sep 17 00:00:00 2001 From: emadum Date: Tue, 9 Feb 2021 14:46:22 -0500 Subject: [PATCH 02/13] add versioned api tests to evergreen --- .evergreen/config.yml | 26 ++++++- .evergreen/config.yml.in | 7 +- .evergreen/generate_evergreen_tasks.js | 22 ++++++ .evergreen/run-tests.sh | 2 +- src/cmap/auth/scram.ts | 11 ++- src/cmap/connect.ts | 13 +++- src/cmap/connection.ts | 7 +- src/connection_string.ts | 12 ++- src/mongo_client.ts | 19 ++--- src/operations/command.ts | 20 ++++- src/operations/find.ts | 6 +- src/operations/update.ts | 2 +- src/sdam/server.ts | 4 +- src/sdam/topology.ts | 9 ++- src/transactions.ts | 5 ++ test/functional/abstract_cursor.test.js | 8 +- test/functional/apm.test.js | 11 ++- test/functional/bulk.test.js | 8 +- test/functional/cmap/connection.test.js | 76 ++++++++++--------- test/functional/collations.test.js | 2 +- test/functional/core/topology.test.js | 23 +++--- test/functional/crud_spec.test.js | 4 + test/functional/cursor.test.js | 7 +- test/functional/gridfs_stream.test.js | 10 ++- test/functional/insert.test.js | 5 +- test/functional/max_staleness.test.js | 8 +- test/functional/operation_example.test.js | 7 +- .../operation_generators_example.test.js | 6 +- .../operation_promises_example.test.js | 2 +- test/functional/promote_values.test.js | 1 + test/functional/spec-runner/index.js | 3 +- .../unified-spec-runner/entities.ts | 8 +- test/functional/unified-spec-runner/match.ts | 9 +-- test/functional/unified-spec-runner/runner.ts | 7 +- .../unified-spec-runner/unified-utils.ts | 4 +- test/functional/uri.test.js | 2 +- test/functional/versioned-api.test.js | 23 +++--- test/functional/view.test.js | 2 +- test/tools/runner/config.js | 17 +++-- .../runner/filters/api_version_filter.js | 37 +++++++++ test/tools/runner/index.js | 10 ++- test/unit/legacy_compat.test.js | 11 ++- 42 files changed, 334 insertions(+), 142 deletions(-) create mode 100755 test/tools/runner/filters/api_version_filter.js diff --git a/.evergreen/config.yml b/.evergreen/config.yml index eb5ca2092cf..8156eac5937 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -81,6 +81,7 @@ functions: MONGODB_VERSION=${VERSION} TOPOLOGY=${TOPOLOGY} \ AUTH=${AUTH} SSL=${SSL} \ ORCHESTRATION_FILE=${ORCHESTRATION_FILE} \ + REQUIRE_API_VERSION=${REQUIRE_API_VERSION} \ bash ${DRIVERS_TOOLS}/.evergreen/run-orchestration.sh - command: expansions.update params: @@ -126,8 +127,10 @@ functions: rm -f ./prepare_client_encryption.sh fi - AUTH=${AUTH} SSL=${SSL} UNIFIED=${UNIFIED} MONGODB_URI="${MONGODB_URI}" \ - NODE_VERSION=${NODE_VERSION} SKIP_DEPS=1 NO_EXIT=1 \ + MONGODB_URI="${MONGODB_URI}" \ + AUTH=${AUTH} SSL=${SSL} UNIFIED=${UNIFIED} \ + MONGODB_API_VERSION="${MONGODB_API_VERSION}" \ + NODE_VERSION=${NODE_VERSION} SKIP_DEPS=${SKIP_DEPS|1} NO_EXIT=${NO_EXIT|1} \ bash ${PROJECT_DIRECTORY}/.evergreen/run-tests.sh run checks: - command: shell.exec @@ -777,6 +780,22 @@ tasks: VERSION: '2.6' TOPOLOGY: sharded_cluster - func: run tests + - name: test-latest-server-v1-api + tags: + - latest + - server + - v1-api + commands: + - func: install dependencies + - func: bootstrap mongo-orchestration + vars: + VERSION: latest + TOPOLOGY: server + REQUIRE_API_VERSION: '1' + - func: run tests + vars: + MONGODB_API_VERSION: '1' + NO_EXIT: '' - name: test-atlas-connectivity tags: - atlas-connect @@ -1195,6 +1214,7 @@ buildvariants: - test-2.6-server - test-2.6-replica_set - test-2.6-sharded_cluster + - test-latest-server-v1-api - test-atlas-connectivity - test-atlas-data-lake - test-auth-kerberos @@ -1261,6 +1281,7 @@ buildvariants: - test-2.6-server - test-2.6-replica_set - test-2.6-sharded_cluster + - test-latest-server-v1-api - test-atlas-connectivity - test-atlas-data-lake - test-auth-kerberos @@ -1356,6 +1377,7 @@ buildvariants: - test-3.2-server - test-3.2-replica_set - test-3.2-sharded_cluster + - test-latest-server-v1-api - test-atlas-connectivity - test-atlas-data-lake - test-auth-kerberos diff --git a/.evergreen/config.yml.in b/.evergreen/config.yml.in index f40c5b3b097..f44c43fd39d 100644 --- a/.evergreen/config.yml.in +++ b/.evergreen/config.yml.in @@ -98,6 +98,7 @@ functions: MONGODB_VERSION=${VERSION} TOPOLOGY=${TOPOLOGY} \ AUTH=${AUTH} SSL=${SSL} \ ORCHESTRATION_FILE=${ORCHESTRATION_FILE} \ + REQUIRE_API_VERSION=${REQUIRE_API_VERSION} \ bash ${DRIVERS_TOOLS}/.evergreen/run-orchestration.sh # run-orchestration generates expansion file with the MONGODB_URI for the cluster - command: expansions.update @@ -146,8 +147,10 @@ functions: rm -f ./prepare_client_encryption.sh fi - AUTH=${AUTH} SSL=${SSL} UNIFIED=${UNIFIED} MONGODB_URI="${MONGODB_URI}" \ - NODE_VERSION=${NODE_VERSION} SKIP_DEPS=1 NO_EXIT=1 \ + MONGODB_URI="${MONGODB_URI}" \ + AUTH=${AUTH} SSL=${SSL} UNIFIED=${UNIFIED} \ + MONGODB_API_VERSION="${MONGODB_API_VERSION}" \ + NODE_VERSION=${NODE_VERSION} SKIP_DEPS=${SKIP_DEPS|1} NO_EXIT=${NO_EXIT|1} \ bash ${PROJECT_DIRECTORY}/.evergreen/run-tests.sh "run checks": diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index f45e40095b8..c01b2d7b04b 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -88,6 +88,28 @@ const BASE_TASKS = []; MONGODB_VERSIONS.forEach(mongoVersion => { TOPOLOGIES.forEach(topology => BASE_TASKS.push(makeTask({ mongoVersion, topology }))); }); +BASE_TASKS.push({ + name: `test-latest-server-v1-api`, + tags: ['latest', 'server', 'v1-api'], + commands: [ + { func: 'install dependencies' }, + { + func: 'bootstrap mongo-orchestration', + vars: { + VERSION: 'latest', + TOPOLOGY: 'server', + REQUIRE_API_VERSION: '1' + } + }, + { + func: 'run tests', + vars: { + MONGODB_API_VERSION: '1', + NO_EXIT: '' + } + } + ] +}); // manually added tasks Array.prototype.push.apply(TASKS, [ diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index dbb756b5599..447f543ee8f 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -56,4 +56,4 @@ else npm install mongodb-client-encryption fi -MONGODB_UNIFIED_TOPOLOGY=${UNIFIED} MONGODB_URI=${MONGODB_URI} npm run ${TEST_NPM_SCRIPT} +MONGODB_API_VERSION=${MONGODB_API_VERSION} MONGODB_UNIFIED_TOPOLOGY=${UNIFIED} MONGODB_URI=${MONGODB_URI} npm run ${TEST_NPM_SCRIPT} diff --git a/src/cmap/auth/scram.ts b/src/cmap/auth/scram.ts index d7362d81326..fa2e8d98481 100644 --- a/src/cmap/auth/scram.ts +++ b/src/cmap/auth/scram.ts @@ -2,7 +2,7 @@ import * as crypto from 'crypto'; import { Binary, Document } from '../../bson'; import { MongoError, AnyError } from '../../error'; import { AuthProvider, AuthContext } from './auth_provider'; -import { Callback, ns } from '../../utils'; +import { Callback, ns, applyServerApiVersion } from '../../utils'; import type { MongoCredentials } from './mongo_credentials'; import type { HandshakeDocument } from '../connect'; @@ -112,6 +112,9 @@ function executeScram(cryptoMethod: CryptoMethod, authContext: AuthContext, call const db = credentials.source; const saslStartCmd = makeFirstMessage(cryptoMethod, credentials, nonce); + if (connection.serverApi) { + applyServerApiVersion(saslStartCmd, connection.serverApi); + } connection.command(ns(`${db}.$cmd`), saslStartCmd, undefined, (_err, result) => { const err = resolveError(_err, result); if (err) { @@ -197,6 +200,9 @@ function continueScramConversation( conversationId: response.conversationId, payload: new Binary(Buffer.from(clientFinal)) }; + if (connection.serverApi) { + applyServerApiVersion(saslContinueCmd, connection.serverApi); + } connection.command(ns(`${db}.$cmd`), saslContinueCmd, undefined, (_err, r) => { const err = resolveError(_err, r); @@ -219,6 +225,9 @@ function continueScramConversation( conversationId: r.conversationId, payload: Buffer.alloc(0) }; + if (connection.serverApi) { + applyServerApiVersion(retrySaslContinueCmd, connection.serverApi); + } connection.command(ns(`${db}.$cmd`), retrySaslContinueCmd, undefined, callback); }); diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 5673180dceb..6e89a70b26a 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -4,7 +4,14 @@ import { Connection, ConnectionOptions, CryptoConnection } from './connection'; import { MongoError, MongoNetworkError, MongoNetworkTimeoutError, AnyError } from '../error'; import { AUTH_PROVIDERS, AuthMechanism } from './auth/defaultAuthProviders'; import { AuthContext } from './auth/auth_provider'; -import { makeClientMetadata, ClientMetadata, Callback, CallbackWithType, ns } from '../utils'; +import { + makeClientMetadata, + ClientMetadata, + Callback, + CallbackWithType, + ns, + applyServerApiVersion +} from '../utils'; import { MAX_SUPPORTED_WIRE_VERSION, MAX_SUPPORTED_SERVER_VERSION, @@ -96,6 +103,10 @@ function performInitialHandshake( handshakeOptions.socketTimeout = options.connectTimeoutMS; } + if (conn.serverApi) { + applyServerApiVersion(handshakeDoc, conn.serverApi); + } + const start = new Date().getTime(); conn.command(ns('admin.$cmd'), handshakeDoc, handshakeOptions, (err, response) => { if (err) { diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index 3063444c07f..62060ba90b8 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -11,8 +11,7 @@ import { Callback, MongoDBNamespace, maxWireVersion, - HostAddress, - applyServerApiVersion + HostAddress } from '../utils'; import { AnyError, @@ -367,10 +366,6 @@ export class Connection extends EventEmitter { const inTransaction = session && (session.inTransaction() || isTransactionCommand(finalCmd)); - if (!inTransaction && !finalCmd.getMore && this.serverApi) { - applyServerApiVersion(finalCmd, this.serverApi); - } - const commandResponseHandler = inTransaction ? (err?: AnyError, ...args: Document[]) => { // We need to add a TransientTransactionError errorLabel, as stated in the transaction spec. diff --git a/src/connection_string.ts b/src/connection_string.ts index 133b7b8940d..0772f37b428 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -21,7 +21,8 @@ import { MongoClient, MongoClientOptions, MongoOptions, - PkFactory + PkFactory, + ServerApi } from './mongo_client'; import { MongoCredentials } from './cmap/auth/mongo_credentials'; import type { TagSet } from './sdam/server_description'; @@ -572,6 +573,15 @@ export const OPTIONS = { autoEncryption: { type: 'record' }, + serverApi: { + target: 'serverApi', + transform({ values: [version] }): ServerApi { + if (typeof version === 'string') { + return { version }; + } + return version as ServerApi; + } + }, checkKeys: { type: 'boolean' }, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index e5bd762feb6..a549cf9bf23 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -222,6 +222,8 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC logger?: Logger; /** Enable command monitoring for this client */ monitorCommands?: boolean; + /** Server API version */ + serverApi?: ServerApiVersion | ServerApi; /** Optionally enable client side auto encryption */ autoEncryption?: AutoEncryptionOptions; /** Allows a wrapping driver to amend the client metadata generated by the driver to include information about the wrapping driver */ @@ -243,13 +245,13 @@ export interface MongoClientPrivate { readConcern?: ReadConcern; writeConcern?: WriteConcern; readPreference: ReadPreference; + serverApi: ServerApi; bsonOptions: BSONSerializeOptions; namespace: MongoDBNamespace; logger: Logger; } const kOptions = Symbol('options'); -const kServerApi = Symbol('serverApi'); /** * The **MongoClient** class is a class that allows for making Connections to MongoDB. @@ -302,24 +304,17 @@ export class MongoClient extends EventEmitter { */ [kOptions]: MongoOptions; - /** - * The MongoDB Server API version - * @internal - * */ - [kServerApi]: ServerApi; - // debugging originalUri; originalOptions; - constructor(url: string, options?: MongoClientOptions, serverApi?: ServerApi) { + constructor(url: string, options?: MongoClientOptions) { super(); this.originalUri = url; this.originalOptions = options; this[kOptions] = parseOptions(url, this, options); - this[kServerApi] = Object.freeze({ version: ServerApiVersion.v1, ...serverApi }); // The internal state this.s = { @@ -329,6 +324,7 @@ export class MongoClient extends EventEmitter { readConcern: this[kOptions].readConcern, writeConcern: this[kOptions].writeConcern, readPreference: this[kOptions].readPreference, + serverApi: this[kOptions].serverApi, bsonOptions: resolveBSONOptions(this[kOptions]), namespace: ns('admin'), logger: this[kOptions].logger @@ -339,8 +335,8 @@ export class MongoClient extends EventEmitter { return Object.freeze({ ...this[kOptions] }); } - get serverApi(): Readonly { - return this[kServerApi]; + get serverApi(): Readonly { + return this[kOptions].serverApi && Object.freeze({ ...this[kOptions].serverApi }); } get autoEncrypter(): AutoEncrypter | undefined { @@ -652,6 +648,7 @@ export interface MongoOptions credentials?: MongoCredentials; readPreference: ReadPreference; readConcern: ReadConcern; + serverApi: ServerApi; writeConcern: WriteConcern; dbName: string; metadata: ClientMetadata; diff --git a/src/operations/command.ts b/src/operations/command.ts index 199862c4b26..7af1799dbfe 100644 --- a/src/operations/command.ts +++ b/src/operations/command.ts @@ -1,7 +1,13 @@ import { Aspect, AbstractOperation, OperationOptions } from './operation'; import { ReadConcern } from '../read_concern'; import { WriteConcern, WriteConcernOptions } from '../write_concern'; -import { maxWireVersion, MongoDBNamespace, Callback, decorateWithExplain } from '../utils'; +import { + applyServerApiVersion, + maxWireVersion, + MongoDBNamespace, + Callback, + decorateWithExplain +} from '../utils'; import type { ReadPreference } from '../read_preference'; import { ClientSession, commandSupportsReadConcern } from '../sessions'; import { MongoError } from '../error'; @@ -173,6 +179,18 @@ export abstract class CommandOperation extends AbstractOperation { } } + // if an API version was declared, add the apiVersion option to every command, except: + // a. only in the initial command of a transaction + // b. only in a Cursor's initiating command, not subsequent getMore commands + if ( + server.serverApi && + (!inTransaction || this.session.transaction.isStarting) && + !cmd.commitTransaction && + !cmd.getMore + ) { + applyServerApiVersion(cmd, server.serverApi); + } + server.command(this.ns, cmd, { fullResult: !!this.fullResponse, ...options }, callback); } } diff --git a/src/operations/find.ts b/src/operations/find.ts index 93864c89488..c3939ec5635 100644 --- a/src/operations/find.ts +++ b/src/operations/find.ts @@ -4,7 +4,8 @@ import { MongoDBNamespace, Callback, normalizeHintField, - decorateWithExplain + decorateWithExplain, + applyServerApiVersion } from '../utils'; import { MongoError } from '../error'; import type { Document } from '../bson'; @@ -153,6 +154,9 @@ export class FindOperation extends CommandOperation { if (this.explain) { findCommand = decorateWithExplain(findCommand, this.explain); } + if (server.serverApi) { + applyServerApiVersion(findCommand, server.serverApi); + } server.command( this.ns, diff --git a/src/operations/update.ts b/src/operations/update.ts index 197c934ab8f..b3057ebaebe 100644 --- a/src/operations/update.ts +++ b/src/operations/update.ts @@ -274,7 +274,7 @@ export function makeUpdateStatement( op.upsert = options.upsert; } - if (typeof options.multi === 'boolean') { + if (options.multi) { op.multi = options.multi; } diff --git a/src/sdam/server.ts b/src/sdam/server.ts index 7c495b6896c..d37d97f4917 100644 --- a/src/sdam/server.ts +++ b/src/sdam/server.ts @@ -44,6 +44,7 @@ import type { ServerHeartbeatSucceededEvent } from './events'; import type { ClientSession } from '../sessions'; import type { Document, Long } from '../bson'; import type { AutoEncrypter } from '../deps'; +import type { ServerApi } from '../mongo_client'; // Used for filtering out fields for logging const DEBUG_FIELDS = [ @@ -106,6 +107,7 @@ export interface ServerPrivate { export class Server extends EventEmitter { /** @internal */ s: ServerPrivate; + serverApi?: ServerApi; clusterTime?: ClusterTime; ismaster?: Document; [kMonitor]: Monitor; @@ -131,7 +133,7 @@ export class Server extends EventEmitter { constructor(topology: Topology, description: ServerDescription, options: ServerOptions) { super(); - options.serverApi = topology.serverApi; + this.serverApi = options.serverApi = topology.serverApi; const poolOptions = { hostAddress: description.hostAddress, ...options }; diff --git a/src/sdam/topology.ts b/src/sdam/topology.ts index 58f5f245af6..216c497e660 100644 --- a/src/sdam/topology.ts +++ b/src/sdam/topology.ts @@ -22,7 +22,8 @@ import { ClientMetadata, Callback, HostAddress, - ns + ns, + applyServerApiVersion } from '../utils'; import { TopologyType, @@ -381,7 +382,11 @@ export class Topology extends EventEmitter { // TODO: NODE-2471 if (server && this.s.credentials) { - server.command(ns('admin.$cmd'), { ping: 1 }, err => { + const pingCmd = { ping: 1 }; + if (server.serverApi) { + applyServerApiVersion(pingCmd, server.serverApi); + } + server.command(ns('admin.$cmd'), pingCmd, err => { if (err) { typeof callback === 'function' ? callback(err) : this.emit(Topology.ERROR, err); return; diff --git a/src/transactions.ts b/src/transactions.ts index 78300490302..9e657046543 100644 --- a/src/transactions.ts +++ b/src/transactions.ts @@ -113,6 +113,11 @@ export class Transaction { return !!this.server; } + /** @returns Whether the transaction has started */ + get isStarting(): boolean { + return this.state === TxnState.STARTING_TRANSACTION; + } + /** * @returns Whether this session is presently in a transaction */ diff --git a/test/functional/abstract_cursor.test.js b/test/functional/abstract_cursor.test.js index c0c18b1c06f..53e6951ed49 100644 --- a/test/functional/abstract_cursor.test.js +++ b/test/functional/abstract_cursor.test.js @@ -143,9 +143,9 @@ describe('AbstractCursor', function () { }); context('#tryNext', function () { - it( - 'should return control to the user if an empty batch is returned', - withClientV2(function (client, done) { + it('should return control to the user if an empty batch is returned', { + metadata: { requires: { apiVersion: false } }, + test: withClientV2(function (client, done) { const db = client.db(); db.createCollection('try_next', { capped: true, size: 10000000 }, () => { const coll = db.collection('try_next'); @@ -173,7 +173,7 @@ describe('AbstractCursor', function () { }); }); }) - ); + }); }); context('#clone', function () { diff --git a/test/functional/apm.test.js b/test/functional/apm.test.js index 347a570b23e..94b3b582cb1 100644 --- a/test/functional/apm.test.js +++ b/test/functional/apm.test.js @@ -332,7 +332,7 @@ describe('APM', function () { ); it('should correctly receive the APM events for a find with getmore and killcursor', { - metadata: { requires: { topology: ['single', 'replicaset'] } }, + metadata: { requires: { apiVersion: false, topology: ['single', 'replicaset'] } }, test: function () { const self = this; @@ -786,7 +786,9 @@ describe('APM', function () { }); it('should correctly decorate the apm result for listCollections with cursorId', { - metadata: { requires: { topology: ['single', 'replicaset'], mongodb: '>=3.0.0' } }, + metadata: { + requires: { apiVersion: false, topology: ['single', 'replicaset'], mongodb: '>=3.0.0' } + }, test: function () { const self = this; const started = []; @@ -1061,6 +1063,11 @@ describe('APM', function () { ); } + // FIXME: NODE-2950 + if (test.description.match(/event with a getmore/)) { + requirements.apiVersion = false; + } + it(test.description, { metadata: { requires: requirements }, test: function () { diff --git a/test/functional/bulk.test.js b/test/functional/bulk.test.js index bdef079b9f7..a6db63d5782 100644 --- a/test/functional/bulk.test.js +++ b/test/functional/bulk.test.js @@ -68,7 +68,7 @@ describe('Bulk', function () { var op = error.getOperation(); test.equal(2, op.q.b); test.equal(1, op.u['$set'].a); - test.equal(false, op.multi); + expect(op.multi).to.not.be.true; test.equal(true, op.upsert); // Get the first error @@ -324,7 +324,7 @@ describe('Bulk', function () { test.ok(error.errmsg != null); test.equal(2, error.getOperation().q.b); test.equal(1, error.getOperation().u['$set'].a); - test.equal(false, error.getOperation().multi); + expect(error.getOperation().multi).to.not.be.true; test.equal(true, error.getOperation().upsert); // Finish up test @@ -698,7 +698,7 @@ describe('Bulk', function () { var op = error.getOperation(); test.equal(2, op.q.b); test.equal(1, op.u['$set'].a); - test.equal(false, op.multi); + expect(op.multi).to.not.be.true; test.equal(true, op.upsert); // Get the first error @@ -1180,7 +1180,6 @@ describe('Bulk', function () { expect(batches[1].operations[0]).to.containSubset({ q: { b: 2 }, u: { $set: { a: 1 } }, - multi: false, upsert: true }); expect(batches[2].operations[0]).to.containSubset({ b: 3, a: 2 }); @@ -1283,7 +1282,6 @@ describe('Bulk', function () { expect(batches[1].operations[0]).to.containSubset({ q: { b: 2 }, u: { $set: { a: 1 } }, - multi: false, upsert: true }); client.close(done); diff --git a/test/functional/cmap/connection.test.js b/test/functional/cmap/connection.test.js index f59a128d5ed..4e46c4c7465 100644 --- a/test/functional/cmap/connection.test.js +++ b/test/functional/cmap/connection.test.js @@ -11,48 +11,54 @@ describe('Connection - functional/cmap', function () { return setupDatabase(this.configuration); }); - it('should execute a command against a server', function (done) { - const connectOptions = Object.assign( - { connectionType: Connection }, - this.configuration.options - ); - - connect(connectOptions, (err, conn) => { - expect(err).to.not.exist; - this.defer(_done => conn.destroy(_done)); + it('should execute a command against a server', { + metadata: { requires: { apiVersion: false } }, + test: function (done) { + const connectOptions = Object.assign( + { connectionType: Connection }, + this.configuration.options + ); - conn.command(ns('admin.$cmd'), { ismaster: 1 }, undefined, (err, ismaster) => { + connect(connectOptions, (err, conn) => { expect(err).to.not.exist; - expect(ismaster).to.exist; - expect(ismaster.ok).to.equal(1); - done(); + this.defer(_done => conn.destroy(_done)); + + conn.command(ns('admin.$cmd'), { ismaster: 1 }, undefined, (err, ismaster) => { + expect(err).to.not.exist; + expect(ismaster).to.exist; + expect(ismaster.ok).to.equal(1); + done(); + }); }); - }); + } }); - it('should emit command monitoring events', function (done) { - const connectOptions = Object.assign( - { connectionType: Connection, monitorCommands: true }, - this.configuration.options - ); - - connect(connectOptions, (err, conn) => { - expect(err).to.not.exist; - this.defer(_done => conn.destroy(_done)); - - const events = []; - conn.on('commandStarted', event => events.push(event)); - conn.on('commandSucceeded', event => events.push(event)); - conn.on('commandFailed', event => events.push(event)); + it('should emit command monitoring events', { + metadata: { requires: { apiVersion: false } }, + test: function (done) { + const connectOptions = Object.assign( + { connectionType: Connection, monitorCommands: true }, + this.configuration.options + ); - conn.command(ns('admin.$cmd'), { ismaster: 1 }, undefined, (err, ismaster) => { + connect(connectOptions, (err, conn) => { expect(err).to.not.exist; - expect(ismaster).to.exist; - expect(ismaster.ok).to.equal(1); - expect(events).to.have.length(2); - done(); + this.defer(_done => conn.destroy(_done)); + + const events = []; + conn.on('commandStarted', event => events.push(event)); + conn.on('commandSucceeded', event => events.push(event)); + conn.on('commandFailed', event => events.push(event)); + + conn.command(ns('admin.$cmd'), { ismaster: 1 }, undefined, (err, ismaster) => { + expect(err).to.not.exist; + expect(ismaster).to.exist; + expect(ismaster.ok).to.equal(1); + expect(events).to.have.length(2); + done(); + }); }); - }); + } }); it.skip('should support socket timeouts', { @@ -78,7 +84,7 @@ describe('Connection - functional/cmap', function () { }); it('should support calling back multiple times on exhaust commands', { - metadata: { requires: { mongodb: '>=4.2.0', topology: ['single'] } }, + metadata: { requires: { apiVersion: false, mongodb: '>=4.2.0', topology: ['single'] } }, test: function (done) { const namespace = ns(`${this.configuration.db}.$cmd`); const connectOptions = Object.assign( diff --git a/test/functional/collations.test.js b/test/functional/collations.test.js index 85468f70495..41fe8e2349e 100644 --- a/test/functional/collations.test.js +++ b/test/functional/collations.test.js @@ -687,7 +687,7 @@ describe('Collation', function () { .collection('test') .createIndex({ a: 1 }, { collation: { caseLevel: true } }) .then(() => { - expect(commandResult).to.eql({ + expect(commandResult).to.containSubset({ createIndexes: 'test', indexes: [{ name: 'a_1', key: { a: 1 }, collation: { caseLevel: true } }] }); diff --git a/test/functional/core/topology.test.js b/test/functional/core/topology.test.js index aca9f80c9bb..980c5b94a08 100644 --- a/test/functional/core/topology.test.js +++ b/test/functional/core/topology.test.js @@ -2,18 +2,21 @@ const expect = require('chai').expect; describe('Topology', function () { - it('should correctly track states of a topology', function (done) { - const topology = this.configuration.newTopology(); + it('should correctly track states of a topology', { + metadata: { requires: { apiVersion: false } }, + test: function (done) { + const topology = this.configuration.newTopology(); - const states = []; - topology.on('stateChanged', (_, newState) => states.push(newState)); - topology.connect(err => { - expect(err).to.not.exist; - topology.destroy(err => { + const states = []; + topology.on('stateChanged', (_, newState) => states.push(newState)); + topology.connect(err => { expect(err).to.not.exist; - expect(states).to.eql(['connecting', 'connected', 'closing', 'closed']); - done(); + topology.destroy(err => { + expect(err).to.not.exist; + expect(states).to.eql(['connecting', 'connected', 'closing', 'closed']); + done(); + }); }); - }); + } }); }); diff --git a/test/functional/crud_spec.test.js b/test/functional/crud_spec.test.js index 563ca1124bb..88af3840330 100644 --- a/test/functional/crud_spec.test.js +++ b/test/functional/crud_spec.test.js @@ -69,6 +69,10 @@ describe('CRUD spec', function () { describe(scenarioName, function () { scenario.tests.forEach(scenarioTest => { beforeEach(() => testContext.db.dropDatabase()); + // FIXME: NODE-2950 + if (scenarioTest.description.match(/Find with limit, sort, and batchsize/)) { + metadata.requires.apiVersion = false; + } it(scenarioTest.description, { metadata, test: function () { diff --git a/test/functional/cursor.test.js b/test/functional/cursor.test.js index f33ebd1063c..a63c65a964f 100644 --- a/test/functional/cursor.test.js +++ b/test/functional/cursor.test.js @@ -2762,7 +2762,10 @@ describe('Cursor', function () { // Add a tag that our runner can trigger on // in this case we are setting that node needs to be higher than 0.10.X to run metadata: { - requires: { topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] } + requires: { + apiVersion: false, + topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] + } }, test: function (done) { @@ -3676,6 +3679,7 @@ describe('Cursor', function () { { metadata: { requires: { + apiVersion: false, topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'], mongodb: '>=3.6.0' } @@ -3710,6 +3714,7 @@ describe('Cursor', function () { { metadata: { requires: { + apiVersion: false, topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'], mongodb: '>=3.6.0' } diff --git a/test/functional/gridfs_stream.test.js b/test/functional/gridfs_stream.test.js index dc3d7938638..1423feb77b4 100644 --- a/test/functional/gridfs_stream.test.js +++ b/test/functional/gridfs_stream.test.js @@ -601,7 +601,7 @@ describe('GridFS Stream', function () { * @example-method abort */ it('Destroying a download stream', { - metadata: { requires: { topology: ['single'] } }, + metadata: { requires: { topology: ['single'], apiVersion: false } }, test: function (done) { var configuration = this.configuration; @@ -669,7 +669,9 @@ describe('GridFS Stream', function () { * @example-method delete */ it('Deleting a file using promises', { - metadata: { requires: { topology: ['single'], node: '>12.0.0' } }, + metadata: { + requires: { topology: ['single'], node: '>12.0.0', sessions: { skipLeakTests: true } } + }, test: function (done) { var configuration = this.configuration; @@ -718,7 +720,7 @@ describe('GridFS Stream', function () { }); it('find()', { - metadata: { requires: { topology: ['single'] } }, + metadata: { requires: { topology: ['single'], sessions: { skipLeakTests: true } } }, test: function (done) { var configuration = this.configuration; @@ -759,7 +761,7 @@ describe('GridFS Stream', function () { * @example-method drop */ it('drop example', { - metadata: { requires: { topology: ['single'] } }, + metadata: { requires: { topology: ['single'], sessions: { skipLeakTests: true } } }, test: function (done) { var configuration = this.configuration; diff --git a/test/functional/insert.test.js b/test/functional/insert.test.js index d5d83471cfc..8a469a8b3cc 100644 --- a/test/functional/insert.test.js +++ b/test/functional/insert.test.js @@ -1840,7 +1840,10 @@ describe('Insert', function () { // Add a tag that our runner can trigger on // in this case we are setting that node needs to be higher than 0.10.X to run metadata: { - requires: { topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] } + requires: { + apiVersion: false, + topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] + } }, test: function (done) { diff --git a/test/functional/max_staleness.test.js b/test/functional/max_staleness.test.js index d87f4336ad0..3de4377fd98 100644 --- a/test/functional/max_staleness.test.js +++ b/test/functional/max_staleness.test.js @@ -63,7 +63,7 @@ describe('Max Staleness', function () { .find({}) .toArray(function (err) { expect(err).to.not.exist; - expect(test.checkCommand).to.eql({ + expect(test.checkCommand).to.containSubset({ $query: { find: 'test', filter: {} }, $readPreference: { mode: 'secondary', maxStalenessSeconds: 250 } }); @@ -98,7 +98,7 @@ describe('Max Staleness', function () { .find({}) .toArray(function (err) { expect(err).to.not.exist; - expect(test.checkCommand).to.eql({ + expect(test.checkCommand).to.containSubset({ $query: { find: 'test', filter: {} }, $readPreference: { mode: 'secondary', maxStalenessSeconds: 250 } }); @@ -134,7 +134,7 @@ describe('Max Staleness', function () { .find({}) .toArray(function (err) { expect(err).to.not.exist; - expect(test.checkCommand).to.eql({ + expect(test.checkCommand).to.containSubset({ $query: { find: 'test', filter: {} }, $readPreference: { mode: 'secondary', maxStalenessSeconds: 250 } }); @@ -169,7 +169,7 @@ describe('Max Staleness', function () { .withReadPreference(readPreference) .toArray(function (err) { expect(err).to.not.exist; - expect(test.checkCommand).to.eql({ + expect(test.checkCommand).to.containSubset({ $query: { find: 'test', filter: {} }, $readPreference: { mode: 'secondary', maxStalenessSeconds: 250 } }); diff --git a/test/functional/operation_example.test.js b/test/functional/operation_example.test.js index 2d645cd61db..8d720fb0a86 100644 --- a/test/functional/operation_example.test.js +++ b/test/functional/operation_example.test.js @@ -117,6 +117,7 @@ describe('Operation Examples', function () { // in this case we are setting that node needs to be higher than 0.10.X to run metadata: { requires: { + apiVersion: false, mongodb: '>2.1.0', topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] } @@ -203,6 +204,7 @@ describe('Operation Examples', function () { // in this case we are setting that node needs to be higher than 0.10.X to run metadata: { requires: { + apiVersion: false, mongodb: '>2.1.0', topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] } @@ -378,6 +380,7 @@ describe('Operation Examples', function () { // in this case we are setting that node needs to be higher than 0.10.X to run metadata: { requires: { + apiVersion: false, mongodb: '>2.1.0', topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] } @@ -466,6 +469,7 @@ describe('Operation Examples', function () { // in this case we are setting that node needs to be higher than 0.10.X to run metadata: { requires: { + apiVersion: false, mongodb: '>2.1.0', topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] } @@ -560,6 +564,7 @@ describe('Operation Examples', function () { // in this case we are setting that node needs to be higher than 0.10.X to run metadata: { requires: { + apiVersion: false, mongodb: '>2.1.0', topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] } @@ -3513,7 +3518,7 @@ describe('Operation Examples', function () { * @example-method removeUser */ it('shouldCorrectlyAddAndRemoveUser', { - metadata: { requires: { topology: 'single' } }, + metadata: { requires: { apiVersion: false, topology: 'single' } }, test: function (done) { var configuration = this.configuration; diff --git a/test/functional/operation_generators_example.test.js b/test/functional/operation_generators_example.test.js index e9faf7c23a2..1b280ab442f 100644 --- a/test/functional/operation_generators_example.test.js +++ b/test/functional/operation_generators_example.test.js @@ -24,7 +24,9 @@ describe('Operation (Generators)', function () { it('aggregationExample2WithGenerators', { // Add a tag that our runner can trigger on // in this case we are setting that node needs to be higher than 0.10.X to run - metadata: { requires: { generators: true, mongodb: '>2.1.0', topology: ['single'] } }, + metadata: { + requires: { apiVersion: false, generators: true, mongodb: '>2.1.0', topology: ['single'] } + }, test: function () { var configuration = this.configuration; @@ -2561,7 +2563,7 @@ describe('Operation (Generators)', function () { * @example-method removeUser */ it('shouldCorrectlyAddAndRemoveUserWithGenerators', { - metadata: { requires: { generators: true, topology: 'single' } }, + metadata: { requires: { apiVersion: false, generators: true, topology: 'single' } }, test: function () { var configuration = this.configuration; diff --git a/test/functional/operation_promises_example.test.js b/test/functional/operation_promises_example.test.js index 9414fdcf537..0d7ecad1e48 100644 --- a/test/functional/operation_promises_example.test.js +++ b/test/functional/operation_promises_example.test.js @@ -33,7 +33,7 @@ describe('Operation (Promises)', function () { it('aggregationExample2WithPromises', { // Add a tag that our runner can trigger on // in this case we are setting that node needs to be higher than 0.10.X to run - metadata: { requires: { mongodb: '>2.1.0', topology: ['single'] } }, + metadata: { requires: { apiVersion: false, mongodb: '>2.1.0', topology: ['single'] } }, test: function () { var configuration = this.configuration; diff --git a/test/functional/promote_values.test.js b/test/functional/promote_values.test.js index c301a1c497c..5ee64ac49d4 100644 --- a/test/functional/promote_values.test.js +++ b/test/functional/promote_values.test.js @@ -208,6 +208,7 @@ describe('Promote Values', function () { it('Should correctly promoteValues when calling getMore on queries', { metadata: { requires: { + apiVersion: false, topology: ['single', 'ssl', 'wiredtiger'] } }, diff --git a/test/functional/spec-runner/index.js b/test/functional/spec-runner/index.js index 1e6bc40ea96..77d92a0caad 100644 --- a/test/functional/spec-runner/index.js +++ b/test/functional/spec-runner/index.js @@ -125,7 +125,8 @@ function generateTopologyTests(testSuites, testContext, filter) { environmentRequirementList.forEach(requires => { const suiteName = `${testSuite.name} - ${requires.topology.join()}`; describe(suiteName, { - metadata: { requires }, + // FIXME: calling this.skip() inside tests triggers the leak checker, disable until fixed + metadata: { requires, sessions: { skipLeakTests: true } }, test: function () { beforeEach(() => prepareDatabaseForSuite(testSuite, testContext)); afterEach(() => testContext.cleanupAfterSuite()); diff --git a/test/functional/unified-spec-runner/entities.ts b/test/functional/unified-spec-runner/entities.ts index 32df9a3f31e..6eff0a283c5 100644 --- a/test/functional/unified-spec-runner/entities.ts +++ b/test/functional/unified-spec-runner/entities.ts @@ -37,7 +37,13 @@ export class UnifiedMongoClient extends MongoClient { } as const; constructor(url: string, description: ClientEntity) { - super(url, { monitorCommands: true, ...description.uriOptions }, description.serverApi); + super(url, { + monitorCommands: true, + ...description.uriOptions, + serverApi: + description.serverApi || + (process.env.MONGODB_API_VERSION && { version: process.env.MONGODB_API_VERSION }) + }); this.events = []; this.failPoints = []; this.ignoredEvents = [ diff --git a/test/functional/unified-spec-runner/match.ts b/test/functional/unified-spec-runner/match.ts index 609bac590c3..671344ed7b7 100644 --- a/test/functional/unified-spec-runner/match.ts +++ b/test/functional/unified-spec-runner/match.ts @@ -129,11 +129,10 @@ export function resultCheck( if (depth > 1) { expect(actual, `Expected actual to exist at ${path.join('')}`).to.exist; - // expect( - // Object.keys(actual), - // `[${Object.keys(actual)}] length !== [${Object.keys(expected)}]` - // ).to.have.lengthOf(Object.keys(expected).length); - expect(Object.keys(actual)).to.include.members(Object.keys(expected)); + expect( + Object.keys(actual), + `[${Object.keys(actual)}] length !== [${Object.keys(expected)}]` + ).to.have.lengthOf(Object.keys(expected).length); } for (const [key, value] of expectedEntries) { diff --git a/test/functional/unified-spec-runner/runner.ts b/test/functional/unified-spec-runner/runner.ts index d4dfc378f6a..1d3e5b9cbab 100644 --- a/test/functional/unified-spec-runner/runner.ts +++ b/test/functional/unified-spec-runner/runner.ts @@ -76,11 +76,12 @@ export async function runUnifiedTest( ...(test.runOnRequirements ?? []) ]; - let doesNotMeetRunOnRequirement = allRequirements.length > 0; + let doesNotMeetRunOnRequirement = false; for (const requirement of allRequirements) { - if (await topologySatisfies(ctx.configuration, requirement, utilClient)) { - doesNotMeetRunOnRequirement = false; // it does meet a run on requirement! + const met = await topologySatisfies(ctx.configuration, requirement, utilClient); + if (!met) { + doesNotMeetRunOnRequirement = true; // it doesn't meet a run on requirement break; } } diff --git a/test/functional/unified-spec-runner/unified-utils.ts b/test/functional/unified-spec-runner/unified-utils.ts index e55ea468812..420df85cbbb 100644 --- a/test/functional/unified-spec-runner/unified-utils.ts +++ b/test/functional/unified-spec-runner/unified-utils.ts @@ -33,9 +33,9 @@ export async function topologySatisfies( Sharded: 'sharded' }[config.topologyType]; - if (r.topologies.includes('sharded-replicaset')) { + if (r.topologies.includes('sharded-replicaset') && topologyType === 'sharded') { const shards = await utilClient.db('config').collection('shards').find({}).toArray(); - ok &&= shards.every(shard => shard.host.split(',').length > 1); + ok &&= shards.length > 0 && shards.every(shard => shard.host.split(',').length > 1); } else { if (!topologyType) throw new Error(`Topology undiscovered: ${config.topologyType}`); ok &&= r.topologies.includes(topologyType); diff --git a/test/functional/uri.test.js b/test/functional/uri.test.js index 90787b9b4da..9f887c7289d 100644 --- a/test/functional/uri.test.js +++ b/test/functional/uri.test.js @@ -74,7 +74,7 @@ describe('URI', function () { it('should correctly connect using uri encoded username and password', { // Add a tag that our runner can trigger on // in this case we are setting that node needs to be higher than 0.10.X to run - metadata: { requires: { topology: 'single' } }, + metadata: { requires: { topology: 'single', apiVersion: false } }, test: function (done) { var self = this; diff --git a/test/functional/versioned-api.test.js b/test/functional/versioned-api.test.js index 6372e5b84be..06d94d709dc 100644 --- a/test/functional/versioned-api.test.js +++ b/test/functional/versioned-api.test.js @@ -9,16 +9,19 @@ describe('Versioned API', function () { expect(versionedApiTest).to.exist; context(String(versionedApiTest.description), function () { for (const test of versionedApiTest.tests) { - it(String(test.description), async function () { - try { - await runUnifiedTest(this, versionedApiTest, test); - } catch (error) { - if (error.message.includes('not implemented.')) { - console.log(`${test.description}: was skipped due to missing functionality`); - console.log(error.stack); - this.skip(); - } else { - throw error; + it(String(test.description), { + metadata: { sessions: { skipLeakTests: true } }, + test: async function () { + try { + await runUnifiedTest(this, versionedApiTest, test); + } catch (error) { + if (error.message.includes('not implemented.')) { + console.log(`${test.description}: was skipped due to missing functionality`); + console.log(error.stack); + this.skip(); + } else { + throw error; + } } } }); diff --git a/test/functional/view.test.js b/test/functional/view.test.js index d853a7812c3..4fee4d454da 100644 --- a/test/functional/view.test.js +++ b/test/functional/view.test.js @@ -58,7 +58,7 @@ describe('Views', function () { ) { expect(r).to.exist; expect(err).to.not.exist; - expect(commandResult).to.eql({ + expect(commandResult).to.containSubset({ create: 'test', viewOn: 'users', pipeline: [{ $match: {} }] diff --git a/test/tools/runner/config.js b/test/tools/runner/config.js index 56aac6d6228..34b2435fe99 100644 --- a/test/tools/runner/config.js +++ b/test/tools/runner/config.js @@ -34,13 +34,13 @@ function convertToConnStringMap(obj) { } class TestConfiguration { - constructor(uri, context, serverApiVersion) { + constructor(uri, context) { const { url, hosts } = parseURI(uri); const hostAddresses = hosts.map(HostAddress.fromString); this.topologyType = context.topologyType; this.version = context.version; - this.serverApiVersion = serverApiVersion; this.clientSideEncryption = context.clientSideEncryption; + this.serverApi = context.serverApi; this.parameters = undefined; this.options = { hosts, @@ -97,17 +97,17 @@ class TestConfiguration { } newClient(dbOptions, serverOptions) { + const defaultOptions = { minHeartbeatFrequencyMS: 100 }; + if (this.serverApi) { + Object.assign(defaultOptions, { serverApi: this.serverApi }); + } // support MongoClient constructor form (url, options) for `newClient` if (typeof dbOptions === 'string') { - return new MongoClient( - dbOptions, - Object.assign({ minHeartbeatFrequencyMS: 100 }, serverOptions), - { version: this.serverApiVersion } - ); + return new MongoClient(dbOptions, Object.assign(defaultOptions, serverOptions)); } dbOptions = dbOptions || {}; - serverOptions = Object.assign({}, { minHeartbeatFrequencyMS: 100 }, serverOptions); + serverOptions = Object.assign({}, defaultOptions, serverOptions); // Fall back let dbHost = (serverOptions && serverOptions.host) || this.options.host; @@ -166,6 +166,7 @@ class TestConfiguration { if (Reflect.has(serverOptions, 'host') || Reflect.has(serverOptions, 'port')) { throw new Error(`Cannot use options to specify host/port, must be in ${connectionString}`); } + return new MongoClient(connectionString, serverOptions); } diff --git a/test/tools/runner/filters/api_version_filter.js b/test/tools/runner/filters/api_version_filter.js new file mode 100755 index 00000000000..83c00b20b70 --- /dev/null +++ b/test/tools/runner/filters/api_version_filter.js @@ -0,0 +1,37 @@ +'use strict'; + +/** + * Filter for the MongoDB API Version required for the test + * + * example: + * metadata: { + * requires: { + * apiVersion: '1' + * } + * } + */ +class ApiVersionFilter { + constructor() { + // Get environmental variables that are known + this.apiVersion = process.env.MONGODB_API_VERSION; + } + + filter(test) { + if (!test.metadata) return true; + if (!test.metadata.requires) return true; + const apiVersion = test.metadata.requires.apiVersion; + + // setting to false skips this test when an apiVersion is required + if (apiVersion === false) return !this.apiVersion; + // setting to true requires some apiVersion be specified + if (apiVersion === true) return !!this.apiVersion; + + // if there's no metadata requirement, always run + if (apiVersion == null) return true; + + // otherwise attempt a direct match + return apiVersion === this.apiVersion; + } +} + +module.exports = ApiVersionFilter; diff --git a/test/tools/runner/index.js b/test/tools/runner/index.js index 14586caf6c1..1caa6fa6b07 100644 --- a/test/tools/runner/index.js +++ b/test/tools/runner/index.js @@ -61,7 +61,8 @@ before(function (_done) { // )} topology` // ); - const client = new MongoClient(MONGODB_URI); + const options = MONGODB_API_VERSION ? { serverApi: MONGODB_API_VERSION } : {}; + const client = new MongoClient(MONGODB_URI, options); const done = err => client.close(err2 => _done(err || err2)); client.connect(err => { @@ -76,9 +77,14 @@ before(function (_done) { return; } + // Ensure test MongoClients set a serverApi parameter when required + if (MONGODB_API_VERSION) { + Object.assign(context, { serverApi: MONGODB_API_VERSION }); + } + // replace this when mocha supports dynamic skipping with `afterEach` filterOutTests(this._runnable.parent); - this.configuration = new TestConfiguration(MONGODB_URI, context, MONGODB_API_VERSION); + this.configuration = new TestConfiguration(MONGODB_URI, context); done(); }); }); diff --git a/test/unit/legacy_compat.test.js b/test/unit/legacy_compat.test.js index 8d7a4e0f8d9..d8fd20a6c31 100644 --- a/test/unit/legacy_compat.test.js +++ b/test/unit/legacy_compat.test.js @@ -1,11 +1,10 @@ 'use strict'; const { expect } = require('chai'); -const { MongoClient } = require('../../src'); describe('Legacy 3.x features', function () { it('Should have bson defined on topology', function () { - const client = new MongoClient(this.configuration.url()); + const client = this.configuration.newClient(this.configuration.url()); return client .connect() .then(client => { @@ -18,13 +17,13 @@ describe('Legacy 3.x features', function () { it('Should allow legacy option useUnifiedTopology', function () { const url = this.configuration.url(); - expect(() => new MongoClient(url, { useUnifiedTopology: true })).to.not.throw; - expect(() => new MongoClient(url, { useUnifiedTopology: false })).to.not.throw; + expect(() => this.configuration.newClient(url, { useUnifiedTopology: true })).to.not.throw; + expect(() => this.configuration.newClient(url, { useUnifiedTopology: false })).to.not.throw; }); it('Should allow legacy option useNewUrlParser', function () { const url = this.configuration.url(); - expect(() => new MongoClient(url, { useNewUrlParser: true })).to.not.throw; - expect(() => new MongoClient(url, { useNewUrlParser: false })).to.not.throw; + expect(() => this.configuration.newClient(url, { useNewUrlParser: true })).to.not.throw; + expect(() => this.configuration.newClient(url, { useNewUrlParser: false })).to.not.throw; }); }); From 92d13d3497766bb7533960b0c78942f707f66a6c Mon Sep 17 00:00:00 2001 From: emadum Date: Fri, 12 Feb 2021 15:02:52 -0500 Subject: [PATCH 03/13] fix --- test/functional/unified-spec-runner/entities.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/functional/unified-spec-runner/entities.ts b/test/functional/unified-spec-runner/entities.ts index 6eff0a283c5..79f42f2b31c 100644 --- a/test/functional/unified-spec-runner/entities.ts +++ b/test/functional/unified-spec-runner/entities.ts @@ -40,9 +40,11 @@ export class UnifiedMongoClient extends MongoClient { super(url, { monitorCommands: true, ...description.uriOptions, - serverApi: - description.serverApi || - (process.env.MONGODB_API_VERSION && { version: process.env.MONGODB_API_VERSION }) + serverApi: description.serverApi + ? description.serverApi + : process.env.MONGODB_API_VERSION + ? { version: process.env.MONGODB_API_VERSION } + : null }); this.events = []; this.failPoints = []; From a6e8951e97e70664c838f518c61545d6ab15ab20 Mon Sep 17 00:00:00 2001 From: emadum Date: Fri, 5 Mar 2021 11:55:42 -0500 Subject: [PATCH 04/13] fixes --- src/index.ts | 1 + src/mongo_client.ts | 7 +++++-- test/functional/core/topology.test.js | 8 +++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 38853ce1bb5..0f925df7449 100644 --- a/src/index.ts +++ b/src/index.ts @@ -213,6 +213,7 @@ export type { MongoOptions, ServerApi, ServerApiVersion, + ServerApiVersionId, SupportedNodeConnectionOptions, SupportedTLSConnectionOptions, SupportedTLSSocketOptions, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 4767f3985b0..2b25bde3267 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -45,9 +45,12 @@ export const ServerApiVersion = { v1: '1' }; +/** @public */ +export type ServerApiVersionId = typeof ServerApiVersion[keyof typeof ServerApiVersion]; + /** @public */ export interface ServerApi { - version: string | typeof ServerApiVersion; + version: string | ServerApiVersionId; strict?: boolean; deprecationErrors?: boolean; } @@ -222,7 +225,7 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC /** Enable command monitoring for this client */ monitorCommands?: boolean; /** Server API version */ - serverApi?: ServerApiVersion | ServerApi; + serverApi?: ServerApi | ServerApiVersionId; /** Optionally enable client side auto encryption */ autoEncryption?: AutoEncryptionOptions; /** Allows a wrapping driver to amend the client metadata generated by the driver to include information about the wrapping driver */ diff --git a/test/functional/core/topology.test.js b/test/functional/core/topology.test.js index 72cceaab0eb..677052c309f 100644 --- a/test/functional/core/topology.test.js +++ b/test/functional/core/topology.test.js @@ -13,11 +13,9 @@ describe('Topology', function () { expect(err).to.not.exist; topology.close(err => { expect(err).to.not.exist; - topology.destroy(err => { - expect(err).to.not.exist; - expect(states).to.eql(['connecting', 'connected', 'closing', 'closed']); - done(); - }); + expect(topology.isDestroyed()).to.be.true; + expect(states).to.eql(['connecting', 'connected', 'closing', 'closed']); + done(); }); }); } From d52d72fdba5f18e8a9f37b9bdcbbea565da9fb1d Mon Sep 17 00:00:00 2001 From: emadum Date: Fri, 5 Mar 2021 12:38:01 -0500 Subject: [PATCH 05/13] skip error 85 index test on latest --- test/functional/index.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/index.test.js b/test/functional/index.test.js index d3246843a58..3f90b7cfbc4 100644 --- a/test/functional/index.test.js +++ b/test/functional/index.test.js @@ -1174,7 +1174,7 @@ describe('Indexes', function () { metadata: { requires: { topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'], - mongodb: '>=3.0.0' + mongodb: '>=3.0.0 <=4.8.0' } }, From 5b141473644abf3100ffa48cea65e09e1bb792e3 Mon Sep 17 00:00:00 2001 From: emadum Date: Fri, 5 Mar 2021 18:12:37 -0500 Subject: [PATCH 06/13] review feedback --- src/cmap/auth/scram.ts | 11 +---------- src/cmap/connect.ts | 13 +------------ src/cmap/connection.ts | 19 +++++++++++++++++-- src/cursor/abstract_cursor.ts | 6 +----- src/mongo_client.ts | 2 -- src/operations/command.ts | 20 +------------------- src/operations/find.ts | 6 +----- src/sdam/topology.ts | 4 ---- src/utils.ts | 7 ------- 9 files changed, 22 insertions(+), 66 deletions(-) diff --git a/src/cmap/auth/scram.ts b/src/cmap/auth/scram.ts index ba4a7605e14..f2dd08e5fb6 100644 --- a/src/cmap/auth/scram.ts +++ b/src/cmap/auth/scram.ts @@ -2,7 +2,7 @@ import * as crypto from 'crypto'; import { Binary, Document } from '../../bson'; import { MongoError, AnyError } from '../../error'; import { AuthProvider, AuthContext } from './auth_provider'; -import { Callback, ns, applyServerApiVersion, emitWarning } from '../../utils'; +import { Callback, ns, emitWarning } from '../../utils'; import type { MongoCredentials } from './mongo_credentials'; import type { HandshakeDocument } from '../connect'; @@ -112,9 +112,6 @@ function executeScram(cryptoMethod: CryptoMethod, authContext: AuthContext, call const db = credentials.source; const saslStartCmd = makeFirstMessage(cryptoMethod, credentials, nonce); - if (connection.serverApi) { - applyServerApiVersion(saslStartCmd, connection.serverApi); - } connection.command(ns(`${db}.$cmd`), saslStartCmd, undefined, (_err, result) => { const err = resolveError(_err, result); if (err) { @@ -200,9 +197,6 @@ function continueScramConversation( conversationId: response.conversationId, payload: new Binary(Buffer.from(clientFinal)) }; - if (connection.serverApi) { - applyServerApiVersion(saslContinueCmd, connection.serverApi); - } connection.command(ns(`${db}.$cmd`), saslContinueCmd, undefined, (_err, r) => { const err = resolveError(_err, r); @@ -225,9 +219,6 @@ function continueScramConversation( conversationId: r.conversationId, payload: Buffer.alloc(0) }; - if (connection.serverApi) { - applyServerApiVersion(retrySaslContinueCmd, connection.serverApi); - } connection.command(ns(`${db}.$cmd`), retrySaslContinueCmd, undefined, callback); }); diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 6e89a70b26a..5673180dceb 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -4,14 +4,7 @@ import { Connection, ConnectionOptions, CryptoConnection } from './connection'; import { MongoError, MongoNetworkError, MongoNetworkTimeoutError, AnyError } from '../error'; import { AUTH_PROVIDERS, AuthMechanism } from './auth/defaultAuthProviders'; import { AuthContext } from './auth/auth_provider'; -import { - makeClientMetadata, - ClientMetadata, - Callback, - CallbackWithType, - ns, - applyServerApiVersion -} from '../utils'; +import { makeClientMetadata, ClientMetadata, Callback, CallbackWithType, ns } from '../utils'; import { MAX_SUPPORTED_WIRE_VERSION, MAX_SUPPORTED_SERVER_VERSION, @@ -103,10 +96,6 @@ function performInitialHandshake( handshakeOptions.socketTimeout = options.connectTimeoutMS; } - if (conn.serverApi) { - applyServerApiVersion(handshakeDoc, conn.serverApi); - } - const start = new Date().getTime(); conn.command(ns('admin.$cmd'), handshakeDoc, handshakeOptions, (err, response) => { if (err) { diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index 62060ba90b8..4b1e2bd8bc7 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -320,6 +320,23 @@ export class Connection extends EventEmitter { let clusterTime = this.clusterTime; let finalCmd = Object.assign({}, cmd); + const inTransaction = session && (session.inTransaction() || isTransactionCommand(finalCmd)); + + // if an API version was declared, add the apiVersion option to every command, except: + // a. only in the initial command of a transaction + // b. only in a Cursor's initiating command, not subsequent getMore commands + if ( + this.serverApi && + (!inTransaction || session?.transaction.isStarting) && + !cmd.commitTransaction && + !cmd.getMore + ) { + const { version, strict, deprecationErrors } = this.serverApi; + finalCmd.apiVersion = version; + if (strict != null) finalCmd.apiStrict = strict; + if (deprecationErrors != null) finalCmd.apiDeprecationErrors = deprecationErrors; + } + if (hasSessionSupport(this) && session) { if ( session.clusterTime && @@ -364,8 +381,6 @@ export class Connection extends EventEmitter { ? new Msg(cmdNs, finalCmd, commandOptions) : new Query(cmdNs, finalCmd, commandOptions); - const inTransaction = session && (session.inTransaction() || isTransactionCommand(finalCmd)); - const commandResponseHandler = inTransaction ? (err?: AnyError, ...args: Document[]) => { // We need to add a TransientTransactionError errorLabel, as stated in the transaction spec. diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 32504c9ae62..a707adb705c 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -572,10 +572,6 @@ export abstract class AbstractCursor extends EventEmitter { return; } - if (!this[kSession]) { - throw new Error('Should have a session when calling getMore'); - } - server.getMore( cursorNs, cursorId, @@ -624,7 +620,7 @@ function next( if (cursorId == null) { // All cursors must operate within a session, one must be made implicitly if not explicitly provided - if (cursor[kSession] == null /*&& cursor[kTopology].hasSessionSupport()*/) { + if (cursor[kSession] == null && cursor[kTopology].hasSessionSupport()) { cursor[kSession] = cursor[kTopology].startSession({ owner: cursor, explicit: false }); } diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 2b25bde3267..0eace47d2f0 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -246,7 +246,6 @@ export interface MongoClientPrivate { readConcern?: ReadConcern; writeConcern?: WriteConcern; readPreference: ReadPreference; - serverApi: ServerApi; bsonOptions: BSONSerializeOptions; namespace: MongoDBNamespace; logger: Logger; @@ -325,7 +324,6 @@ export class MongoClient extends EventEmitter { readConcern: this[kOptions].readConcern, writeConcern: this[kOptions].writeConcern, readPreference: this[kOptions].readPreference, - serverApi: this[kOptions].serverApi, bsonOptions: resolveBSONOptions(this[kOptions]), namespace: ns('admin'), logger: this[kOptions].logger diff --git a/src/operations/command.ts b/src/operations/command.ts index 7af1799dbfe..199862c4b26 100644 --- a/src/operations/command.ts +++ b/src/operations/command.ts @@ -1,13 +1,7 @@ import { Aspect, AbstractOperation, OperationOptions } from './operation'; import { ReadConcern } from '../read_concern'; import { WriteConcern, WriteConcernOptions } from '../write_concern'; -import { - applyServerApiVersion, - maxWireVersion, - MongoDBNamespace, - Callback, - decorateWithExplain -} from '../utils'; +import { maxWireVersion, MongoDBNamespace, Callback, decorateWithExplain } from '../utils'; import type { ReadPreference } from '../read_preference'; import { ClientSession, commandSupportsReadConcern } from '../sessions'; import { MongoError } from '../error'; @@ -179,18 +173,6 @@ export abstract class CommandOperation extends AbstractOperation { } } - // if an API version was declared, add the apiVersion option to every command, except: - // a. only in the initial command of a transaction - // b. only in a Cursor's initiating command, not subsequent getMore commands - if ( - server.serverApi && - (!inTransaction || this.session.transaction.isStarting) && - !cmd.commitTransaction && - !cmd.getMore - ) { - applyServerApiVersion(cmd, server.serverApi); - } - server.command(this.ns, cmd, { fullResult: !!this.fullResponse, ...options }, callback); } } diff --git a/src/operations/find.ts b/src/operations/find.ts index c3939ec5635..93864c89488 100644 --- a/src/operations/find.ts +++ b/src/operations/find.ts @@ -4,8 +4,7 @@ import { MongoDBNamespace, Callback, normalizeHintField, - decorateWithExplain, - applyServerApiVersion + decorateWithExplain } from '../utils'; import { MongoError } from '../error'; import type { Document } from '../bson'; @@ -154,9 +153,6 @@ export class FindOperation extends CommandOperation { if (this.explain) { findCommand = decorateWithExplain(findCommand, this.explain); } - if (server.serverApi) { - applyServerApiVersion(findCommand, server.serverApi); - } server.command( this.ns, diff --git a/src/sdam/topology.ts b/src/sdam/topology.ts index 4fe61cbf6b9..de8453e7329 100644 --- a/src/sdam/topology.ts +++ b/src/sdam/topology.ts @@ -22,7 +22,6 @@ import { Callback, HostAddress, ns, - applyServerApiVersion, emitWarning } from '../utils'; import { @@ -379,9 +378,6 @@ export class Topology extends EventEmitter { // TODO: NODE-2471 if (server && this.s.credentials) { const pingCmd = { ping: 1 }; - if (server.serverApi) { - applyServerApiVersion(pingCmd, server.serverApi); - } server.command(ns('admin.$cmd'), pingCmd, err => { if (err) { typeof callback === 'function' ? callback(err) : this.emit(Topology.ERROR, err); diff --git a/src/utils.ts b/src/utils.ts index 15020e57dbb..372620bb814 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1370,13 +1370,6 @@ export const DEFAULT_PK_FACTORY = { } }; -export function applyServerApiVersion(finalCmd: Document, serverApi: Document): void { - const { version, strict, deprecationErrors } = serverApi; - finalCmd.apiVersion = version; - if (strict != null) finalCmd.apiStrict = strict; - if (deprecationErrors != null) finalCmd.apiDeprecationErrors = deprecationErrors; -} - /** * When the driver used emitWarning the code will be equal to this. * @public From 4c0310cd3069396dd44bd29d7ce295f43ab20e8b Mon Sep 17 00:00:00 2001 From: emadum Date: Fri, 5 Mar 2021 18:25:07 -0500 Subject: [PATCH 07/13] remove duplicate loglevel exports --- src/mongo_client.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 0eace47d2f0..d9854c23395 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -32,14 +32,6 @@ import type { SrvPoller } from './sdam/srv_polling'; import type { Connection } from './cmap/connection'; import type { LEGAL_TLS_SOCKET_OPTIONS, LEGAL_TCP_SOCKET_OPTIONS } from './cmap/connect'; -/** @public */ -export const LogLevel = { - error: 'error', - warn: 'warn', - info: 'info', - debug: 'debug' -} as const; - /** @public */ export const ServerApiVersion = { v1: '1' @@ -55,9 +47,6 @@ export interface ServerApi { deprecationErrors?: boolean; } -/** @public */ -export type LogLevelId = typeof LogLevel[keyof typeof LogLevel]; - /** @public */ export interface DriverInfo { name?: string; From ec53fcc09b298060952bcd96729035f336d8bfa0 Mon Sep 17 00:00:00 2001 From: emadum Date: Fri, 5 Mar 2021 18:34:13 -0500 Subject: [PATCH 08/13] cleanup --- src/sdam/topology.ts | 3 +-- test/functional/apm.test.js | 4 +--- test/functional/crud_spec.test.js | 4 ---- test/functional/cursor.test.js | 4 +--- test/functional/insert.test.js | 4 +--- test/functional/operation_generators_example.test.js | 4 +--- 6 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/sdam/topology.ts b/src/sdam/topology.ts index de8453e7329..ae7e3e1f9ea 100644 --- a/src/sdam/topology.ts +++ b/src/sdam/topology.ts @@ -377,8 +377,7 @@ export class Topology extends EventEmitter { // TODO: NODE-2471 if (server && this.s.credentials) { - const pingCmd = { ping: 1 }; - server.command(ns('admin.$cmd'), pingCmd, err => { + server.command(ns('admin.$cmd'), { ping: 1 }, err => { if (err) { typeof callback === 'function' ? callback(err) : this.emit(Topology.ERROR, err); return; diff --git a/test/functional/apm.test.js b/test/functional/apm.test.js index 98e5d43a319..347a570b23e 100644 --- a/test/functional/apm.test.js +++ b/test/functional/apm.test.js @@ -786,9 +786,7 @@ describe('APM', function () { }); it('should correctly decorate the apm result for listCollections with cursorId', { - metadata: { - requires: { topology: ['single', 'replicaset'], mongodb: '>=3.0.0' } - }, + metadata: { requires: { topology: ['single', 'replicaset'], mongodb: '>=3.0.0' } }, test: function () { const self = this; const started = []; diff --git a/test/functional/crud_spec.test.js b/test/functional/crud_spec.test.js index 88af3840330..563ca1124bb 100644 --- a/test/functional/crud_spec.test.js +++ b/test/functional/crud_spec.test.js @@ -69,10 +69,6 @@ describe('CRUD spec', function () { describe(scenarioName, function () { scenario.tests.forEach(scenarioTest => { beforeEach(() => testContext.db.dropDatabase()); - // FIXME: NODE-2950 - if (scenarioTest.description.match(/Find with limit, sort, and batchsize/)) { - metadata.requires.apiVersion = false; - } it(scenarioTest.description, { metadata, test: function () { diff --git a/test/functional/cursor.test.js b/test/functional/cursor.test.js index 246a64a3c00..406f557544d 100644 --- a/test/functional/cursor.test.js +++ b/test/functional/cursor.test.js @@ -2762,9 +2762,7 @@ describe('Cursor', function () { // Add a tag that our runner can trigger on // in this case we are setting that node needs to be higher than 0.10.X to run metadata: { - requires: { - topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] - } + requires: { topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] } }, test: function (done) { diff --git a/test/functional/insert.test.js b/test/functional/insert.test.js index 19890dd51a5..ebff4b03cd3 100644 --- a/test/functional/insert.test.js +++ b/test/functional/insert.test.js @@ -1839,9 +1839,7 @@ describe('Insert', function () { // Add a tag that our runner can trigger on // in this case we are setting that node needs to be higher than 0.10.X to run metadata: { - requires: { - topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] - } + requires: { topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] } }, test: function (done) { diff --git a/test/functional/operation_generators_example.test.js b/test/functional/operation_generators_example.test.js index 24f57ca2618..9c89bdf053b 100644 --- a/test/functional/operation_generators_example.test.js +++ b/test/functional/operation_generators_example.test.js @@ -24,9 +24,7 @@ describe('Operation (Generators)', function () { it('aggregationExample2WithGenerators', { // Add a tag that our runner can trigger on // in this case we are setting that node needs to be higher than 0.10.X to run - metadata: { - requires: { generators: true, mongodb: '>2.1.0', topology: ['single'] } - }, + metadata: { requires: { generators: true, mongodb: '>2.1.0', topology: ['single'] } }, test: function () { var configuration = this.configuration; From 88199d216a5883807c4c2c3d8dc11be472f27f53 Mon Sep 17 00:00:00 2001 From: emadum Date: Fri, 5 Mar 2021 20:57:31 -0500 Subject: [PATCH 09/13] fix --- src/cursor/abstract_cursor.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index a707adb705c..9183154636b 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -628,9 +628,7 @@ function next( if (state) { const response = state.response; cursor[kServer] = state.server; - if (state.session) { - cursor[kSession] = state.session; - } + cursor[kSession] = state.session; if (response.cursor) { cursor[kId] = From d388f838cae68807bade86228afe4d5f48d6f619 Mon Sep 17 00:00:00 2001 From: emadum Date: Wed, 17 Mar 2021 10:36:01 -0400 Subject: [PATCH 10/13] review feedback --- src/operations/connect.ts | 2 +- src/sdam/server.ts | 4 +++- src/sdam/topology.ts | 12 +++--------- test/tools/runner/index.js | 2 +- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/operations/connect.ts b/src/operations/connect.ts index 34d95c2c2fe..256054adb46 100644 --- a/src/operations/connect.ts +++ b/src/operations/connect.ts @@ -112,7 +112,7 @@ function createTopology( callback: Callback ) { // Create the topology - const topology = new Topology(options.hosts, options, mongoClient.serverApi); + const topology = new Topology(options.hosts, options); // Events can be emitted before initialization is complete so we have to // save the reference to the topology on the client ASAP if the event handlers need to access it mongoClient.topology = topology; diff --git a/src/sdam/server.ts b/src/sdam/server.ts index ceb070790e7..374d1faee9a 100644 --- a/src/sdam/server.ts +++ b/src/sdam/server.ts @@ -101,6 +101,8 @@ export interface ServerPrivate { topology: Topology; /** A connection pool for this server */ pool: ConnectionPool; + /** MongoDB server API version */ + serverApi?: ServerApi; } /** @public */ @@ -133,7 +135,7 @@ export class Server extends EventEmitter { constructor(topology: Topology, description: ServerDescription, options: ServerOptions) { super(); - this.serverApi = options.serverApi = topology.serverApi; + this.serverApi = options.serverApi; const poolOptions = { hostAddress: description.hostAddress, ...options }; diff --git a/src/sdam/topology.ts b/src/sdam/topology.ts index ae7e3e1f9ea..93b348aee1c 100644 --- a/src/sdam/topology.ts +++ b/src/sdam/topology.ts @@ -139,6 +139,8 @@ export interface TopologyOptions extends BSONSerializeOptions, ServerOptions { /** Indicates that a client should directly connect to a node without attempting to discover its topology type */ directConnection: boolean; metadata: ClientMetadata; + /** MongoDB server API version */ + serverApi?: ServerApi; } /** @public */ @@ -167,8 +169,6 @@ export class Topology extends EventEmitter { ismaster?: Document; /** @internal */ _type?: string; - /** @internal */ - serverApi?: ServerApi; /** @event */ static readonly SERVER_OPENING = 'serverOpening' as const; @@ -201,15 +201,9 @@ export class Topology extends EventEmitter { /** * @param seedlist - a list of HostAddress instances to connect to */ - constructor( - seeds: string | string[] | HostAddress | HostAddress[], - options: TopologyOptions, - serverApi?: ServerApi - ) { + constructor(seeds: string | string[] | HostAddress | HostAddress[], options: TopologyOptions) { super(); - this.serverApi = serverApi; - // Legacy CSFLE support this.bson = Object.create(null); this.bson.serialize = serialize; diff --git a/test/tools/runner/index.js b/test/tools/runner/index.js index 1caa6fa6b07..313f72688e1 100644 --- a/test/tools/runner/index.js +++ b/test/tools/runner/index.js @@ -79,7 +79,7 @@ before(function (_done) { // Ensure test MongoClients set a serverApi parameter when required if (MONGODB_API_VERSION) { - Object.assign(context, { serverApi: MONGODB_API_VERSION }); + context.serverApi = MONGODB_API_VERSION; } // replace this when mocha supports dynamic skipping with `afterEach` From b4893a19579864d0a30f9f5c3188aa57bbcb4a56 Mon Sep 17 00:00:00 2001 From: emadum Date: Wed, 17 Mar 2021 11:58:57 -0400 Subject: [PATCH 11/13] refactor: extract supportsVersionedApi helper function --- src/cmap/connection.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index 4b1e2bd8bc7..73496a53adb 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -322,15 +322,7 @@ export class Connection extends EventEmitter { let finalCmd = Object.assign({}, cmd); const inTransaction = session && (session.inTransaction() || isTransactionCommand(finalCmd)); - // if an API version was declared, add the apiVersion option to every command, except: - // a. only in the initial command of a transaction - // b. only in a Cursor's initiating command, not subsequent getMore commands - if ( - this.serverApi && - (!inTransaction || session?.transaction.isStarting) && - !cmd.commitTransaction && - !cmd.getMore - ) { + if (this.serverApi && supportsVersionedApi(cmd, session)) { const { version, strict, deprecationErrors } = this.serverApi; finalCmd.apiVersion = version; if (strict != null) finalCmd.apiStrict = strict; @@ -649,6 +641,16 @@ function supportsOpMsg(conn: Connection) { return maxWireVersion(conn) >= 6 && !description.__nodejs_mock_server__; } +function supportsVersionedApi(cmd: Document, session?: ClientSession) { + const inTransaction = session && (session.inTransaction() || isTransactionCommand(cmd)); + // if an API version was declared, add the apiVersion option to every command, except: + // a. only in the initial command of a transaction + // b. only in a Cursor's initiating command, not subsequent getMore commands + return ( + (!inTransaction || session?.transaction.isStarting) && !cmd.commitTransaction && !cmd.getMore + ); +} + function messageHandler(conn: Connection) { return function messageHandler(message: BinMsg | Response) { // always emit the message, in case we are streaming From 78f68ebc3cf93602a53c363dbe55c7bf2d978421 Mon Sep 17 00:00:00 2001 From: emadum Date: Wed, 17 Mar 2021 12:00:03 -0400 Subject: [PATCH 12/13] fix: disallow serverApi in uri options --- src/connection_string.ts | 6 ++++++ test/functional/versioned-api.test.js | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/src/connection_string.ts b/src/connection_string.ts index 307b0ab0f86..a119be4ca3c 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -324,6 +324,12 @@ export function parseOptions( throw new MongoParseError('URI cannot contain options with no value'); } + if (key.toLowerCase() === 'serverapi') { + throw new MongoParseError( + 'URI cannot contain `serverApi`, it can only be passed to the client' + ); + } + if (key.toLowerCase() === 'authsource' && urlOptions.has('authSource')) { // If authSource is an explicit key in the urlOptions we need to remove the implicit dbName urlOptions.delete('authSource'); diff --git a/test/functional/versioned-api.test.js b/test/functional/versioned-api.test.js index 06d94d709dc..e16bd772121 100644 --- a/test/functional/versioned-api.test.js +++ b/test/functional/versioned-api.test.js @@ -5,6 +5,15 @@ const { loadSpecTests } = require('../spec/index'); const { runUnifiedTest } = require('./unified-spec-runner/runner'); describe('Versioned API', function () { + it('should throw an error if serverApi version is provided via the uri', { + metadata: { topology: 'single' }, + test: function () { + expect(() => this.configuration.newClient({ serverApi: '1' })).to.throw( + /URI cannot contain `serverApi`, it can only be passed to the client/ + ); + } + }); + for (const versionedApiTest of loadSpecTests('versioned-api')) { expect(versionedApiTest).to.exist; context(String(versionedApiTest.description), function () { From ea5cf9cb6826bb646060911e5900a3e27bf754c8 Mon Sep 17 00:00:00 2001 From: emadum Date: Wed, 17 Mar 2021 12:00:37 -0400 Subject: [PATCH 13/13] fix: dont export ServerApiVersion as type --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 0f925df7449..e6c39b6aa04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -85,6 +85,7 @@ export { Compressor } from './cmap/wire_protocol/compression'; export { ExplainVerbosity } from './explain'; export { ReadConcernLevel } from './read_concern'; export { ReadPreferenceMode } from './read_preference'; +export { ServerApiVersion } from './mongo_client'; // events export { @@ -212,7 +213,6 @@ export type { DriverInfo, MongoOptions, ServerApi, - ServerApiVersion, ServerApiVersionId, SupportedNodeConnectionOptions, SupportedTLSConnectionOptions,