diff --git a/test/functional/sessions.test.js b/test/functional/sessions.test.js index 93b99be5a04..f0e0fb5cb75 100644 --- a/test/functional/sessions.test.js +++ b/test/functional/sessions.test.js @@ -217,7 +217,10 @@ describe('Sessions - functional', function () { 'Server returns an error on listCollections with snapshot', 'Server returns an error on listDatabases with snapshot', 'Server returns an error on listIndexes with snapshot', - 'Server returns an error on runCommand with snapshot' + 'Server returns an error on runCommand with snapshot', + 'Server returns an error on findOneAndUpdate with snapshot', + 'Server returns an error on deleteOne with snapshot', + 'Server returns an error on updateOne with snapshot' ] }; const testsToSkip = skipTestMap[sessionTests.description] || []; diff --git a/test/functional/unified-spec-runner/entities.ts b/test/functional/unified-spec-runner/entities.ts index eccf54ce79a..a1adff3e9f3 100644 --- a/test/functional/unified-spec-runner/entities.ts +++ b/test/functional/unified-spec-runner/entities.ts @@ -11,7 +11,20 @@ import { WriteConcern } from '../../../src/write_concern'; import { ReadPreference } from '../../../src/read_preference'; import { ClientSession } from '../../../src/sessions'; import { ChangeStream } from '../../../src/change_stream'; +import { FindCursor } from '../../../src/cursor/find_cursor'; import type { ClientEntity, EntityDescription } from './schema'; +import type { + ConnectionPoolCreatedEvent, + ConnectionPoolClosedEvent, + ConnectionCreatedEvent, + ConnectionReadyEvent, + ConnectionClosedEvent, + ConnectionCheckOutStartedEvent, + ConnectionCheckOutFailedEvent, + ConnectionCheckedOutEvent, + ConnectionCheckedInEvent, + ConnectionPoolClearedEvent +} from '../../../src/cmap/connection_pool_events'; import type { CommandFailedEvent, CommandStartedEvent, @@ -26,6 +39,17 @@ interface UnifiedChangeStream extends ChangeStream { } export type CommandEvent = CommandStartedEvent | CommandSucceededEvent | CommandFailedEvent; +export type CmapEvent = + | ConnectionPoolCreatedEvent + | ConnectionPoolClosedEvent + | ConnectionCreatedEvent + | ConnectionReadyEvent + | ConnectionClosedEvent + | ConnectionCheckOutStartedEvent + | ConnectionCheckOutFailedEvent + | ConnectionCheckedOutEvent + | ConnectionCheckedInEvent + | ConnectionPoolClearedEvent; function serverApiConfig() { if (process.env.MONGODB_API_VERSION) { @@ -38,52 +62,105 @@ function getClient(address) { return new MongoClient(`mongodb://${address}`, serverApi ? { serverApi } : {}); } +type PushFunction = (e: CommandEvent | CmapEvent) => void; + export class UnifiedMongoClient extends MongoClient { - events: CommandEvent[]; + commandEvents: CommandEvent[]; + cmapEvents: CmapEvent[]; failPoints: Document[]; ignoredEvents: string[]; - observedEvents: ('commandStarted' | 'commandSucceeded' | 'commandFailed')[]; + observedCommandEvents: ('commandStarted' | 'commandSucceeded' | 'commandFailed')[]; + observedCmapEvents: ( + | 'connectionPoolCreated' + | 'connectionPoolClosed' + | 'connectionPoolCleared' + | 'connectionCreated' + | 'connectionReady' + | 'connectionClosed' + | 'connectionCheckOutStarted' + | 'connectionCheckOutFailed' + | 'connectionCheckedOut' + | 'connectionCheckedIn' + )[]; - static EVENT_NAME_LOOKUP = { + static COMMAND_EVENT_NAME_LOOKUP = { commandStartedEvent: 'commandStarted', commandSucceededEvent: 'commandSucceeded', commandFailedEvent: 'commandFailed' } as const; + static CMAP_EVENT_NAME_LOOKUP = { + poolCreatedEvent: 'connectionPoolCreated', + poolClosedEvent: 'connectionPoolClosed', + poolClearedEvent: 'connectionPoolCleared', + connectionCreatedEvent: 'connectionCreated', + connectionReadyEvent: 'connectionReady', + connectionClosedEvent: 'connectionClosed', + connectionCheckOutStartedEvent: 'connectionCheckOutStarted', + connectionCheckOutFailedEvent: 'connectionCheckOutFailed', + connectionCheckedOutEvent: 'connectionCheckedOut', + connectionCheckedInEvent: 'connectionCheckedIn' + } as const; + constructor(url: string, description: ClientEntity) { super(url, { monitorCommands: true, ...description.uriOptions, serverApi: description.serverApi ? description.serverApi : serverApiConfig() }); - this.events = []; + this.commandEvents = []; + this.cmapEvents = []; this.failPoints = []; this.ignoredEvents = [ ...(description.ignoreCommandMonitoringEvents ?? []), 'configureFailPoint' ]; - // apm - this.observedEvents = (description.observeEvents ?? []).map( - e => UnifiedMongoClient.EVENT_NAME_LOOKUP[e] - ); - for (const eventName of this.observedEvents) { - this.on(eventName, this.pushEvent); + this.observedCommandEvents = (description.observeEvents ?? []) + .map(e => UnifiedMongoClient.COMMAND_EVENT_NAME_LOOKUP[e]) + .filter(e => !!e); + this.observedCmapEvents = (description.observeEvents ?? []) + .map(e => UnifiedMongoClient.CMAP_EVENT_NAME_LOOKUP[e]) + .filter(e => !!e); + for (const eventName of this.observedCommandEvents) { + this.on(eventName, this.pushCommandEvent); + } + for (const eventName of this.observedCmapEvents) { + this.on(eventName, this.pushCmapEvent); } } - // NOTE: pushEvent must be an arrow function - pushEvent: (e: CommandEvent) => void = e => { - if (!this.ignoredEvents.includes(e.commandName)) { - this.events.push(e); + isIgnored(e: CommandEvent | CmapEvent): boolean { + return this.ignoredEvents.includes(e.commandName); + } + + // NOTE: pushCommandEvent must be an arrow function + pushCommandEvent: (e: CommandEvent) => void = e => { + if (!this.isIgnored(e)) { + this.commandEvents.push(e); } }; - /** Disables command monitoring for the client and returns a list of the captured events. */ - stopCapturingEvents(): CommandEvent[] { - for (const eventName of this.observedEvents) { - this.off(eventName, this.pushEvent); + // NOTE: pushCmapEvent must be an arrow function + pushCmapEvent: (e: CmapEvent) => void = e => { + this.cmapEvents.push(e); + }; + + stopCapturingEvents(pushFn: PushFunction): void { + const observedEvents = this.observedCommandEvents.concat(this.observedCmapEvents); + for (const eventName of observedEvents) { + this.off(eventName, pushFn); } - return this.events; + } + + /** Disables command monitoring for the client and returns a list of the captured events. */ + stopCapturingCommandEvents(): CommandEvent[] { + this.stopCapturingEvents(this.pushCommandEvent); + return this.commandEvents; + } + + stopCapturingCmapEvents(): CmapEvent[] { + this.stopCapturingEvents(this.pushCmapEvent); + return this.cmapEvents; } } @@ -137,6 +214,7 @@ export type Entity = | Db | Collection | ClientSession + | FindCursor | UnifiedChangeStream | GridFSBucket | Document; // Results from operations @@ -147,9 +225,17 @@ export type EntityCtor = | typeof Collection | typeof ClientSession | typeof ChangeStream + | typeof FindCursor | typeof GridFSBucket; -export type EntityTypeId = 'client' | 'db' | 'collection' | 'session' | 'bucket' | 'stream'; +export type EntityTypeId = + | 'client' + | 'db' + | 'collection' + | 'session' + | 'bucket' + | 'cursor' + | 'stream'; const ENTITY_CTORS = new Map(); ENTITY_CTORS.set('client', UnifiedMongoClient); @@ -157,6 +243,7 @@ ENTITY_CTORS.set('db', Db); ENTITY_CTORS.set('collection', Collection); ENTITY_CTORS.set('session', ClientSession); ENTITY_CTORS.set('bucket', GridFSBucket); +ENTITY_CTORS.set('cursor', FindCursor); ENTITY_CTORS.set('stream', ChangeStream); export class EntitiesMap extends Map { @@ -172,6 +259,7 @@ export class EntitiesMap extends Map { mapOf(type: 'collection'): EntitiesMap; mapOf(type: 'session'): EntitiesMap; mapOf(type: 'bucket'): EntitiesMap; + mapOf(type: 'cursor'): EntitiesMap; mapOf(type: 'stream'): EntitiesMap; mapOf(type: EntityTypeId): EntitiesMap { const ctor = ENTITY_CTORS.get(type); @@ -186,6 +274,7 @@ export class EntitiesMap extends Map { getEntity(type: 'collection', key: string, assertExists?: boolean): Collection; getEntity(type: 'session', key: string, assertExists?: boolean): ClientSession; getEntity(type: 'bucket', key: string, assertExists?: boolean): GridFSBucket; + getEntity(type: 'cursor', key: string, assertExists?: boolean): FindCursor; getEntity(type: 'stream', key: string, assertExists?: boolean): UnifiedChangeStream; getEntity(type: EntityTypeId, key: string, assertExists = true): Entity { const entity = this.get(key); @@ -205,11 +294,17 @@ export class EntitiesMap extends Map { async cleanup(): Promise { await this.failPoints.disableFailPoints(); - for (const [, client] of this.mapOf('client')) { - await client.close(); + for (const [, cursor] of this.mapOf('cursor')) { + await cursor.close(); + } + for (const [, stream] of this.mapOf('stream')) { + await stream.close(); } for (const [, session] of this.mapOf('session')) { - await session.endSession(); + await session.endSession({ force: true }); + } + for (const [, client] of this.mapOf('client')) { + await client.close(); } this.clear(); } @@ -222,7 +317,8 @@ export class EntitiesMap extends Map { for (const entity of entities ?? []) { if ('client' in entity) { const useMultipleMongoses = - config.topologyType === 'Sharded' && entity.client.useMultipleMongoses; + (config.topologyType === 'LoadBalanced' || config.topologyType === 'Sharded') && + entity.client.useMultipleMongoses; const uri = config.url({ useMultipleMongoses }); const client = new UnifiedMongoClient(uri, entity.client); await client.connect(); diff --git a/test/functional/unified-spec-runner/match.ts b/test/functional/unified-spec-runner/match.ts index e9644094768..319955a180f 100644 --- a/test/functional/unified-spec-runner/match.ts +++ b/test/functional/unified-spec-runner/match.ts @@ -1,12 +1,25 @@ import { expect } from 'chai'; +import { inspect } from 'util'; import { Binary, Document, Long, ObjectId, MongoError } from '../../../src'; import { CommandFailedEvent, CommandStartedEvent, CommandSucceededEvent } from '../../../src/cmap/command_monitoring_events'; -import { CommandEvent, EntitiesMap } from './entities'; -import { ExpectedError, ExpectedEvent } from './schema'; +import { + ConnectionPoolCreatedEvent, + ConnectionPoolClosedEvent, + ConnectionCreatedEvent, + ConnectionReadyEvent, + ConnectionClosedEvent, + ConnectionCheckOutStartedEvent, + ConnectionCheckOutFailedEvent, + ConnectionCheckedOutEvent, + ConnectionCheckedInEvent, + ConnectionPoolClearedEvent +} from '../../../src/cmap/connection_pool_events'; +import { CommandEvent, CmapEvent, EntitiesMap } from './entities'; +import { ExpectedCmapEvent, ExpectedCommandEvent, ExpectedError } from './schema'; export interface ExistsOperator { $$exists: boolean; @@ -235,32 +248,70 @@ export function specialCheck( } } +// CMAP events where the payload does not matter. +const EMPTY_CMAP_EVENTS = { + poolCreatedEvent: ConnectionPoolCreatedEvent, + poolClosedEvent: ConnectionPoolClosedEvent, + connectionCreatedEvent: ConnectionCreatedEvent, + connectionReadyEvent: ConnectionReadyEvent, + connectionCheckOutStartedEvent: ConnectionCheckOutStartedEvent, + connectionCheckOutFailedEvent: ConnectionCheckOutFailedEvent, + connectionCheckedOutEvent: ConnectionCheckedOutEvent, + connectionCheckedInEvent: ConnectionCheckedInEvent +}; + +function validEmptyCmapEvent( + expected: ExpectedCommandEvent | ExpectedCmapEvent, + actual: CommandEvent | CmapEvent +) { + return Object.values(EMPTY_CMAP_EVENTS).some(value => { + return actual instanceof value; + }); +} + export function matchesEvents( - expected: ExpectedEvent[], - actual: CommandEvent[], + expected: (ExpectedCommandEvent & ExpectedCmapEvent)[], + actual: (CommandEvent & CmapEvent)[], entities: EntitiesMap ): void { - // TODO: NodeJS Driver has extra events - // expect(actual).to.have.lengthOf(expected.length); + if (actual.length !== expected.length) { + const actualNames = actual.map(a => a.constructor.name); + const expectedNames = expected.map(e => Object.keys(e)[0]); + expect.fail( + `Expected event count mismatch, expected ${inspect(expectedNames)} but got ${inspect( + actualNames + )}` + ); + } for (const [index, actualEvent] of actual.entries()) { const expectedEvent = expected[index]; - if (expectedEvent.commandStartedEvent && actualEvent instanceof CommandStartedEvent) { + if (expectedEvent.commandStartedEvent) { + expect(actualEvent).to.be.instanceOf(CommandStartedEvent); resultCheck(actualEvent, expectedEvent.commandStartedEvent, entities, [ `events[${index}].commandStartedEvent` ]); - } else if ( - expectedEvent.commandSucceededEvent && - actualEvent instanceof CommandSucceededEvent - ) { + } else if (expectedEvent.commandSucceededEvent) { + expect(actualEvent).to.be.instanceOf(CommandSucceededEvent); resultCheck(actualEvent, expectedEvent.commandSucceededEvent, entities, [ `events[${index}].commandSucceededEvent` ]); - } else if (expectedEvent.commandFailedEvent && actualEvent instanceof CommandFailedEvent) { + } else if (expectedEvent.commandFailedEvent) { + expect(actualEvent).to.be.instanceOf(CommandFailedEvent); expect(actualEvent.commandName).to.equal(expectedEvent.commandFailedEvent.commandName); - } else { - expect.fail(`Events must be one of the known types, got ${actualEvent}`); + } else if (expectedEvent.connectionClosedEvent) { + expect(actualEvent).to.be.instanceOf(ConnectionClosedEvent); + if (expectedEvent.connectionClosedEvent.hasServiceId) { + expect(actualEvent).property('serviceId').to.exist; + } + } else if (expectedEvent.poolClearedEvent) { + expect(actualEvent).to.be.instanceOf(ConnectionPoolClearedEvent); + if (expectedEvent.poolClearedEvent.hasServiceId) { + expect(actualEvent).property('serviceId').to.exist; + } + } else if (!validEmptyCmapEvent(expectedEvent, actualEvent)) { + expect.fail(`Events must be one of the known types, got ${inspect(actualEvent)}`); } } } diff --git a/test/functional/unified-spec-runner/operations.ts b/test/functional/unified-spec-runner/operations.ts index 26fcd8df830..40aa0f864ab 100644 --- a/test/functional/unified-spec-runner/operations.ts +++ b/test/functional/unified-spec-runner/operations.ts @@ -121,9 +121,9 @@ operations.set('assertDifferentLsidOnLastTwoCommands', async ({ entities, operat operations.set('assertSameLsidOnLastTwoCommands', async ({ entities, operation }) => { const client = entities.getEntity('client', operation.arguments.client); - expect(client.observedEvents.includes('commandStarted')).to.be.true; + expect(client.observedCommandEvents.includes('commandStarted')).to.be.true; - const startedEvents = client.events.filter( + const startedEvents = client.commandEvents.filter( ev => ev instanceof CommandStartedEvent ) as CommandStartedEvent[]; @@ -173,11 +173,34 @@ operations.set('assertSessionTransactionState', async ({ entities, operation }) expect(session.transaction.state).to.equal(driverTransactionStateName); }); +operations.set('assertNumberConnectionsCheckedOut', async ({ entities, operation }) => { + const client = entities.getEntity('client', operation.arguments.client); + const servers = Array.from(client.topology.s.servers.values()); + const checkedOutConnections = servers.reduce((count, server) => { + const pool = server.s.pool; + return count + pool.currentCheckedOutCount; + }, 0); + expect(checkedOutConnections).to.equal(operation.arguments.connections); +}); + operations.set('bulkWrite', async ({ entities, operation }) => { const collection = entities.getEntity('collection', operation.object); return collection.bulkWrite(operation.arguments.requests); }); +// The entity exists for the name but can potentially have the wrong +// type (stream/cursor) which will also throw an exception even when +// telling getEntity() to ignore checking existence. +operations.set('close', async ({ entities, operation }) => { + try { + const cursor = entities.getEntity('cursor', operation.object); + await cursor.close(); + } catch (e) { + const changeStream = entities.getEntity('stream', operation.object); + await changeStream.close(); + } +}); + operations.set('commitTransaction', async ({ entities, operation }) => { const session = entities.getEntity('session', operation.object); return session.commitTransaction(); @@ -219,6 +242,17 @@ operations.set('createCollection', async ({ entities, operation }) => { }); }); +operations.set('createFindCursor', async ({ entities, operation }) => { + const collection = entities.getEntity('collection', operation.object); + const { filter, sort, batchSize, limit, let: vars } = operation.arguments; + const cursor = collection.find(filter, { sort, batchSize, limit, let: vars }); + // The spec dictates that we create the cursor and force the find command + // to execute, but don't move the cursor forward. hasNext() accomplishes + // this. + await cursor.hasNext(); + return cursor; +}); + operations.set('createIndex', async ({ entities, operation }) => { const collection = entities.getEntity('collection', operation.object); const session = entities.getEntity('session', operation.arguments.session, false); @@ -294,12 +328,23 @@ operations.set('insertMany', async ({ entities, operation }) => { }); operations.set('iterateUntilDocumentOrError', async ({ entities, operation }) => { - const changeStream = entities.getEntity('stream', operation.object); - // Either change or error promise will finish - return Promise.race([ - changeStream.eventCollector.waitAndShiftEvent('change'), - changeStream.eventCollector.waitAndShiftEvent('error') - ]); + try { + const changeStream = entities.getEntity('stream', operation.object); + // Either change or error promise will finish + return Promise.race([ + changeStream.eventCollector.waitAndShiftEvent('change'), + changeStream.eventCollector.waitAndShiftEvent('error') + ]); + } catch (e) { + const findCursor = entities.getEntity('cursor', operation.object); + return await findCursor.next(); + } +}); + +operations.set('listCollections', async ({ entities, operation }) => { + const db = entities.getEntity('db', operation.object); + const { filter, ...opts } = operation.arguments; + return db.listCollections(filter, opts).toArray(); }); operations.set('listDatabases', async ({ entities, operation }) => { @@ -307,6 +352,11 @@ operations.set('listDatabases', async ({ entities, operation }) => { return client.db().admin().listDatabases(); }); +operations.set('listIndexes', async ({ entities, operation }) => { + const collection = entities.getEntity('collection', operation.object); + return collection.listIndexes(operation.arguments).toArray(); +}); + operations.set('replaceOne', async ({ entities, operation }) => { const collection = entities.getEntity('collection', operation.object); return collection.replaceOne(operation.arguments.filter, operation.arguments.replacement, { @@ -441,12 +491,15 @@ export async function executeOperationAndCheck( if (operation.expectError) { expectErrorCheck(error, operation.expectError, entities); return; - } else { + } else if (!operation.ignoreResultAndError) { throw error; } } // We check the positive outcome here so the try-catch above doesn't catch our chai assertions + if (operation.ignoreResultAndError) { + return; + } if (operation.expectError) { expect.fail(`Operation ${operation.name} succeeded but was not supposed to`); diff --git a/test/functional/unified-spec-runner/runner.ts b/test/functional/unified-spec-runner/runner.ts index eb545670af6..6f55f49cd59 100644 --- a/test/functional/unified-spec-runner/runner.ts +++ b/test/functional/unified-spec-runner/runner.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { ReadPreference } from '../../../src/read_preference'; import * as uni from './schema'; import { zip, topologySatisfies, patchVersion } from './unified-utils'; -import { CommandEvent, EntitiesMap } from './entities'; +import { CmapEvent, CommandEvent, EntitiesMap } from './entities'; import { ns } from '../../../src/utils'; import { executeOperationAndCheck } from './operations'; import { matchesEvents } from './match'; @@ -41,7 +41,13 @@ export async function runUnifiedTest( ctx.skip(); } - const utilClient = ctx.configuration.newClient(); + let utilClient; + if (ctx.configuration.isLoadBalanced) { + // The util client can always point at the single mongos LB frontend. + utilClient = ctx.configuration.newClient(ctx.configuration.singleMongosLoadBalancerUri); + } else { + utilClient = ctx.configuration.newClient(); + } let entities; try { @@ -107,7 +113,8 @@ export async function runUnifiedTest( // To ease the implementation, test runners MAY execute distinct before every test. if ( ctx.topologyType === uni.TopologyType.sharded || - ctx.topologyType === uni.TopologyType.shardedReplicaset + ctx.topologyType === uni.TopologyType.shardedReplicaset || + ctx.topologyType === uni.TopologyType.loadBalanced ) { for (const [, collection] of entities.mapOf('collection')) { await utilClient.db(ns(collection.namespace).db).command({ @@ -121,18 +128,27 @@ export async function runUnifiedTest( await executeOperationAndCheck(operation, entities, utilClient); } - const clientEvents = new Map(); + const clientCommandEvents = new Map(); + const clientCmapEvents = 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()); + clientCommandEvents.set(id, client.stopCapturingCommandEvents()); + clientCmapEvents.set(id, client.stopCapturingCmapEvents()); } if (test.expectEvents) { for (const expectedEventList of test.expectEvents) { const clientId = expectedEventList.client; - const actualEvents = clientEvents.get(clientId); - + const eventType = expectedEventList.eventType; + let actualEvents; + // If no event type is provided it defaults to 'command', so just + // check for 'cmap' here for now. + if (eventType === 'cmap') { + actualEvents = clientCmapEvents.get(clientId); + } else { + actualEvents = clientCommandEvents.get(clientId); + } expect(actualEvents, `No client entity found with id ${clientId}`).to.exist; matchesEvents(expectedEventList.events, actualEvents, entities); } diff --git a/test/functional/unified-spec-runner/schema.ts b/test/functional/unified-spec-runner/schema.ts index 41cb00221ae..659f0b6c328 100644 --- a/test/functional/unified-spec-runner/schema.ts +++ b/test/functional/unified-spec-runner/schema.ts @@ -1,4 +1,4 @@ -import type { Document } from '../../../src/bson'; +import type { Document, ObjectId } from '../../../src/bson'; import type { ReadConcernLevel } from '../../../src/read_concern'; import type { ReadPreferenceMode } from '../../../src/read_preference'; import type { TagSet } from '../../../src/sdam/server_description'; @@ -27,7 +27,8 @@ export const TopologyType = Object.freeze({ single: 'single', replicaset: 'replicaset', sharded: 'sharded', - shardedReplicaset: 'sharded-replicaset' + shardedReplicaset: 'sharded-replicaset', + loadBalanced: 'load-balanced' } as const); export type TopologyId = typeof TopologyType[keyof typeof TopologyType]; export interface RunOnRequirement { @@ -36,16 +37,27 @@ export interface RunOnRequirement { topologies?: TopologyId[]; serverParameters?: Document; } -export type ObservableEventId = +export type ObservableCommandEventId = | 'commandStartedEvent' | 'commandSucceededEvent' | 'commandFailedEvent'; +export type ObservableCmapEventId = + | 'connectionPoolCreatedEvent' + | 'connectionPoolClosedEvent' + | 'connectionPoolClearedEvent' + | 'connectionCreatedEvent' + | 'connectionReadyEvent' + | 'connectionClosedEvent' + | 'connectionCheckOutStartedEvent' + | 'connectionCheckOutFailedEvent' + | 'connectionCheckedOutEvent' + | 'connectionCheckedInEvent'; export interface ClientEntity { id: string; uriOptions?: Document; useMultipleMongoses?: boolean; - observeEvents?: ObservableEventId[]; + observeEvents?: (ObservableCommandEventId | ObservableCmapEventId)[]; ignoreCommandMonitoringEvents?: string[]; serverApi?: ServerApi; } @@ -118,9 +130,10 @@ export interface Test { } export interface ExpectedEventsForClient { client: string; - events: ExpectedEvent[]; + eventType?: string; + events: (ExpectedCommandEvent | ExpectedCmapEvent)[]; } -export interface ExpectedEvent { +export interface ExpectedCommandEvent { commandStartedEvent?: { command?: Document; commandName?: string; @@ -134,6 +147,25 @@ export interface ExpectedEvent { commandName?: string; }; } +export interface ExpectedCmapEvent { + poolCreatedEvent?: Record; + poolReadyEvent?: Record; + poolClearedEvent?: { + serviceId?: ObjectId; + hasServiceId?: boolean; + }; + poolClosedEvent?: Record; + connectionCreatedEvent?: Record; + connectionReadyEvent?: Record; + connectionClosedEvent?: { + reason?: string; + hasServiceId?: boolean; + }; + connectionCheckOutStartedEvent?: Record; + connectionCheckOutFailedEvent?: Record; + connectionCheckedOutEvent?: Record; + connectionCheckedInEvent?: Record; +} export interface ExpectedError { isError?: true; isClientError?: boolean; diff --git a/test/functional/unified-spec-runner/unified-utils.ts b/test/functional/unified-spec-runner/unified-utils.ts index ea5b8dfe59a..9f9ad171db3 100644 --- a/test/functional/unified-spec-runner/unified-utils.ts +++ b/test/functional/unified-spec-runner/unified-utils.ts @@ -31,10 +31,14 @@ export async function topologySatisfies( Single: 'single', ReplicaSetNoPrimary: 'replicaset', ReplicaSetWithPrimary: 'replicaset', - Sharded: 'sharded' + Sharded: 'sharded', + LoadBalanced: 'load-balanced' }[config.topologyType]; - if (r.topologies.includes('sharded-replicaset') && topologyType === 'sharded') { + if ( + r.topologies.includes('sharded-replicaset') && + (topologyType === 'sharded' || topologyType === 'load-balanced') + ) { const shards = await utilClient.db('config').collection('shards').find({}).toArray(); ok &&= shards.length > 0 && shards.every(shard => shard.host.split(',').length > 1); } else { @@ -52,6 +56,13 @@ export async function topologySatisfies( } } + if (r.auth) { + ok &&= + !!utilClient.options.auth || + !!utilClient.options.authSource || + !!utilClient.options.authMechanism; + } + return ok; } diff --git a/test/tools/runner/config.js b/test/tools/runner/config.js index 4943a2da1b9..1b02cfa929a 100644 --- a/test/tools/runner/config.js +++ b/test/tools/runner/config.js @@ -43,6 +43,8 @@ class TestConfiguration { this.clientSideEncryption = context.clientSideEncryption; this.serverApi = context.serverApi; this.parameters = undefined; + this.singleMongosLoadBalancerUri = context.singleMongosLoadBalancerUri; + this.multiMongosLoadBalancerUri = context.multiMongosLoadBalancerUri; this.options = { hosts, hostAddresses, @@ -64,6 +66,10 @@ class TestConfiguration { return { writeConcern: { w: 1 } }; } + get isLoadBalanced() { + return !!this.singleMongosLoadBalancerUri && !!this.multiMongosLoadBalancerUri; + } + get host() { return this.options.host; } @@ -206,6 +212,10 @@ class TestConfiguration { if (options.username) url.username = options.username; if (options.password) url.password = options.password; + if (this.isLoadBalanced) { + url.searchParams.append('loadBalanced', true); + } + if (options.username || options.password) { if (options.authMechanism) { url.searchParams.append('authMechanism', options.authMechanism); diff --git a/test/tools/runner/index.js b/test/tools/runner/index.js index 313f72688e1..08a51f90f88 100644 --- a/test/tools/runner/index.js +++ b/test/tools/runner/index.js @@ -10,6 +10,10 @@ const wtfnode = require('wtfnode'); const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017'; const MONGODB_API_VERSION = process.env.MONGODB_API_VERSION; +// Load balancer fronting 1 mongos. +const SINGLE_MONGOS_LB_URI = process.env.SINGLE_MONGOS_LB_URI; +// Load balancer fronting 2 mongoses. +const MULTI_MONGOS_LB_URI = process.env.MULTI_MONGOS_LB_URI; const filters = []; function initializeFilters(client, callback) { @@ -82,6 +86,11 @@ before(function (_done) { context.serverApi = MONGODB_API_VERSION; } + if (SINGLE_MONGOS_LB_URI && MULTI_MONGOS_LB_URI) { + context.singleMongosLoadBalancerUri = SINGLE_MONGOS_LB_URI; + context.multiMongosLoadBalancerUri = MULTI_MONGOS_LB_URI; + } + // replace this when mocha supports dynamic skipping with `afterEach` filterOutTests(this._runnable.parent); this.configuration = new TestConfiguration(MONGODB_URI, context);