diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index aa73e88e5fc..62db2f98d4d 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -222,7 +222,7 @@ export async function prepareHandshakeDocument( const options = authContext.options; const compressors = options.compressors ? options.compressors : []; const { serverApi } = authContext.connection; - const clientMetadata: Document = await options.extendedMetadata; + const clientMetadata: Document = await options.metadata; const handshakeDoc: HandshakeDocument = { [serverApi?.version || options.loadBalanced === true ? 'hello' : LEGACY_HELLO_COMMAND]: 1, diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index d2d7f8bf12e..7c204d09dd2 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -140,9 +140,8 @@ export interface ConnectionOptions socketTimeoutMS?: number; /** @internal */ cancellationToken?: CancellationToken; - metadata: ClientMetadata; /** @internal */ - extendedMetadata: Promise; + metadata: Promise; /** @internal */ mongoLogger?: MongoLogger | undefined; } diff --git a/src/cmap/connection_pool.ts b/src/cmap/connection_pool.ts index a4d931518fb..411295e658e 100644 --- a/src/cmap/connection_pool.ts +++ b/src/cmap/connection_pool.ts @@ -601,9 +601,9 @@ export class ConnectionPool extends TypedEventEmitter { } private createConnection(callback: Callback) { - // Note that metadata and extendedMetadata may have changed on the client but have - // been frozen here, so we pull the extendedMetadata promise always from the client - // no mattter what options were set at the construction of the pool. + // Note that metadata may have changed on the client but have + // been frozen here, so we pull the metadata promise always from the client + // no matter what options were set at the construction of the pool. const connectOptions: ConnectionOptions = { ...this.options, id: this.connectionCounter.next().value, @@ -611,7 +611,7 @@ export class ConnectionPool extends TypedEventEmitter { cancellationToken: this.cancellationToken, mongoLogger: this.mongoLogger, authProviders: this.server.topology.client.s.authProviders, - extendedMetadata: this.server.topology.client.options.extendedMetadata + metadata: this.server.topology.client.options.metadata }; this.pending++; diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index c8737d36aef..aa5cb70e7b2 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -27,8 +27,7 @@ export function isDriverInfoEqual(info1: DriverInfo, info2: DriverInfo): boolean } /** - * @public - * @deprecated This interface will be made internal in the next major release. + * @internal * @see https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.md#hello-command */ export interface ClientMetadata { @@ -48,11 +47,14 @@ export interface ClientMetadata { }; /** FaaS environment information */ env?: { - name: 'aws.lambda' | 'gcp.func' | 'azure.func' | 'vercel'; + name?: 'aws.lambda' | 'gcp.func' | 'azure.func' | 'vercel'; timeout_sec?: Int32; memory_mb?: Int32; region?: string; - url?: string; + container?: { + runtime?: string; + orchestrator?: string; + }; }; } @@ -103,10 +105,10 @@ type MakeClientMetadataOptions = Pick; * 3. Omit the `env` document entirely. * 4. Truncate `platform`. -- special we do not truncate this field */ -export function makeClientMetadata( +export async function makeClientMetadata( driverInfoList: DriverInfo[], { appName = '' }: MakeClientMetadataOptions -): ClientMetadata { +): Promise { const metadataDocument = new LimitedSizeDocument(512); // Add app name first, it must be sent @@ -178,19 +180,21 @@ export function makeClientMetadata( } } } - return metadataDocument.toObject() as ClientMetadata; + return await addContainerMetadata(metadataDocument.toObject() as ClientMetadata); } let dockerPromise: Promise; +type ContainerMetadata = NonNullable['container']>; /** @internal */ -async function getContainerMetadata() { - const containerMetadata: Record = {}; +async function getContainerMetadata(): Promise { dockerPromise ??= fileIsAccessible('/.dockerenv'); const isDocker = await dockerPromise; const { KUBERNETES_SERVICE_HOST = '' } = process.env; const isKubernetes = KUBERNETES_SERVICE_HOST.length > 0 ? true : false; + const containerMetadata: ContainerMetadata = {}; + if (isDocker) containerMetadata.runtime = 'docker'; if (isKubernetes) containerMetadata.orchestrator = 'kubernetes'; @@ -202,15 +206,16 @@ async function getContainerMetadata() { * Re-add each metadata value. * Attempt to add new env container metadata, but keep old data if it does not fit. */ -export async function addContainerMetadata( - originalMetadata: ClientMetadata -): Promise { +async function addContainerMetadata(originalMetadata: ClientMetadata): Promise { const containerMetadata = await getContainerMetadata(); if (Object.keys(containerMetadata).length === 0) return originalMetadata; const extendedMetadata = new LimitedSizeDocument(512); - const extendedEnvMetadata = { ...originalMetadata?.env, container: containerMetadata }; + const extendedEnvMetadata: NonNullable = { + ...originalMetadata?.env, + container: containerMetadata + }; for (const [key, val] of Object.entries(originalMetadata)) { if (key !== 'env') { diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 474edc44f70..ac879b8d585 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -16,7 +16,6 @@ import { AuthMechanism } from './cmap/auth/providers'; import type { LEGAL_TCP_SOCKET_OPTIONS, LEGAL_TLS_SOCKET_OPTIONS } from './cmap/connect'; import type { Connection } from './cmap/connection'; import { - addContainerMetadata, type ClientMetadata, isDriverInfoEqual, makeClientMetadata @@ -408,31 +407,12 @@ export class MongoClient extends TypedEventEmitter implements * The consolidate, parsed, transformed and merged options. */ public readonly options: Readonly< - Omit< - MongoOptions, - | 'monitorCommands' - | 'ca' - | 'crl' - | 'key' - | 'cert' - | 'driverInfo' - | 'additionalDriverInfo' - | 'metadata' - | 'extendedMetadata' - > + Omit > & - Pick< - MongoOptions, - | 'monitorCommands' - | 'ca' - | 'crl' - | 'key' - | 'cert' - | 'driverInfo' - | 'additionalDriverInfo' - | 'metadata' - | 'extendedMetadata' - >; + Pick & { + /** @internal */ + metadata: Promise; + }; private driverInfoList: DriverInfo[] = []; @@ -506,10 +486,9 @@ export class MongoClient extends TypedEventEmitter implements if (isDuplicateDriverInfo) return; this.driverInfoList.push(driverInfo); - this.options.metadata = makeClientMetadata(this.driverInfoList, this.options); - this.options.extendedMetadata = addContainerMetadata(this.options.metadata) + this.options.metadata = makeClientMetadata(this.driverInfoList, this.options) .then(undefined, squashError) - .then(result => result ?? {}); // ensure Promise + .then(result => result ?? ({} as ClientMetadata)); // ensure Promise } /** @internal */ @@ -1105,12 +1084,8 @@ export interface MongoOptions compressors: CompressorName[]; writeConcern: WriteConcern; dbName: string; - /** @deprecated - Will be made internal in a future major release. */ - metadata: ClientMetadata; - /** @deprecated - Will be made internal in a future major release. */ - extendedMetadata: Promise; - /** @deprecated - Will be made internal in a future major release. */ - additionalDriverInfo: DriverInfo[]; + /** @internal */ + metadata: Promise; /** @internal */ autoEncrypter?: AutoEncrypter; /** @internal */ diff --git a/src/sdam/topology.ts b/src/sdam/topology.ts index 5506e1bd015..85e1663d8bf 100644 --- a/src/sdam/topology.ts +++ b/src/sdam/topology.ts @@ -150,8 +150,7 @@ 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; loadBalanced: boolean; - metadata: ClientMetadata; - extendedMetadata: Promise; + metadata: Promise; serverMonitoringMode: ServerMonitoringMode; /** MongoDB server API version */ serverApi?: ServerApi; @@ -750,10 +749,6 @@ export class Topology extends TypedEventEmitter { if (typeof callback === 'function') callback(undefined, true); } - get clientMetadata(): ClientMetadata { - return this.s.options.metadata; - } - isConnected(): boolean { return this.s.state === STATE_CONNECTED; } diff --git a/test/integration/connection-monitoring-and-pooling/connection.test.ts b/test/integration/connection-monitoring-and-pooling/connection.test.ts index 68f678f7a9c..20272b128a6 100644 --- a/test/integration/connection-monitoring-and-pooling/connection.test.ts +++ b/test/integration/connection-monitoring-and-pooling/connection.test.ts @@ -15,10 +15,7 @@ import { } from '../../../src'; import { connect } from '../../../src/cmap/connect'; import { Connection } from '../../../src/cmap/connection'; -import { - addContainerMetadata, - makeClientMetadata -} from '../../../src/cmap/handshake/client_metadata'; +import { makeClientMetadata } from '../../../src/cmap/handshake/client_metadata'; import { MongoDBResponse } from '../../../src/cmap/wire_protocol/responses'; import { LEGACY_HELLO_COMMAND } from '../../../src/constants'; import { Topology } from '../../../src/sdam/topology'; @@ -51,8 +48,7 @@ describe('Connection', function () { ...commonConnectOptions, connectionType: Connection, ...this.configuration.options, - metadata: makeClientMetadata([], {}), - extendedMetadata: addContainerMetadata(makeClientMetadata([], {})) + metadata: makeClientMetadata([], {}) }; let conn; @@ -74,8 +70,7 @@ describe('Connection', function () { connectionType: Connection, ...this.configuration.options, monitorCommands: true, - metadata: makeClientMetadata([], {}), - extendedMetadata: addContainerMetadata(makeClientMetadata([], {})) + metadata: makeClientMetadata([], {}) }; let conn; @@ -106,8 +101,7 @@ describe('Connection', function () { connectionType: Connection, ...this.configuration.options, monitorCommands: true, - metadata: makeClientMetadata([], {}), - extendedMetadata: addContainerMetadata(makeClientMetadata([], {})) + metadata: makeClientMetadata([], {}) }; let conn; diff --git a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts index be5e51645ee..f60c439fe21 100644 --- a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts +++ b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts @@ -149,8 +149,8 @@ describe('Handshake Prose Tests', function () { it('includes both container and FAAS provider information in the client metadata', async function () { client = this.configuration.newClient(); await client.connect(); - expect(client.topology?.s.options.extendedMetadata).to.exist; - const { env } = await client.topology.s.options.extendedMetadata; + expect(client.topology?.s.options.metadata).to.exist; + const { env } = await client.topology.s.options.metadata; expect(env).to.deep.equal({ region: 'us-east-2', diff --git a/test/integration/node-specific/mongo_client.test.ts b/test/integration/node-specific/mongo_client.test.ts index 9e0394013cd..ecc96aa40f6 100644 --- a/test/integration/node-specific/mongo_client.test.ts +++ b/test/integration/node-specific/mongo_client.test.ts @@ -8,16 +8,15 @@ import { type CommandFailedEvent, type CommandStartedEvent, type CommandSucceededEvent, - Connection, Db, - getTopology, MongoClient, MongoNotConnectedError, MongoServerSelectionError, - ReadPreference, - ServerDescription, - Topology -} from '../../mongodb'; + ReadPreference +} from '../../../src'; +import { Connection } from '../../../src/cmap/connection'; +import { ServerDescription } from '../../../src/sdam/server_description'; +import { Topology } from '../../../src/sdam/topology'; import { clearFailPoint, configureFailPoint } from '../../tools/utils'; import { setupDatabase } from '../shared'; @@ -34,16 +33,17 @@ describe('class MongoClient', function () { client = undefined; }); - it('should correctly pass through extra db options', { - metadata: { requires: { topology: ['single'] } }, - test: function (done) { + it( + 'should correctly pass through extra db options', + { requires: { topology: 'single' } }, + async function () { const configuration = this.configuration; const client = configuration.newClient( {}, { writeConcern: { w: 1, wtimeoutMS: 1000, fsync: true, j: true }, readPreference: 'nearest', - readPreferenceTags: { loc: 'ny' }, + readPreferenceTags: [{ loc: 'ny' }], forceServerObjectId: true, pkFactory: { createPk() { @@ -54,74 +54,55 @@ describe('class MongoClient', function () { } ); - client.connect(function (err, client) { - expect(err).to.be.undefined; + await client.connect(); - const db = client.db(configuration.db); + const db = client.db(configuration.db); - expect(db).to.have.property('writeConcern'); - expect(db.writeConcern).to.have.property('w', 1); - expect(db.writeConcern).to.have.property('wtimeoutMS', 1000); - expect(db.writeConcern).to.have.property('journal', true); + expect(db).to.have.property('writeConcern'); + expect(db.writeConcern).to.have.property('w', 1); + expect(db.writeConcern).to.have.property('wtimeoutMS', 1000); + expect(db.writeConcern).to.have.property('journal', true); - expect(db).to.have.property('s'); - expect(db.s).to.have.property('readPreference'); - expect(db.s.readPreference).to.have.property('mode', 'nearest'); - expect(db.s.readPreference) - .to.have.property('tags') - .that.deep.equals([{ loc: 'ny' }]); + expect(db).to.have.property('s'); + expect(db.s).to.have.property('readPreference'); + expect(db.s.readPreference).to.have.property('mode', 'nearest'); + expect(db.s.readPreference) + .to.have.property('tags') + .that.deep.equals([{ loc: 'ny' }]); - expect(db.s).to.have.nested.property('options.forceServerObjectId'); - expect(db.s.options).to.have.property('forceServerObjectId', true); - expect(db.s).to.have.nested.property('pkFactory.createPk').that.is.a('function'); - expect(db.s.pkFactory.createPk()).to.equal(1); - expect(db).to.have.nested.property('bsonOptions.serializeFunctions'); + expect(db.s).to.have.nested.property('options.forceServerObjectId'); + expect(db.s.options).to.have.property('forceServerObjectId', true); + expect(db.s).to.have.nested.property('pkFactory.createPk').that.is.a('function'); + expect(db.s.pkFactory.createPk()).to.equal(1); + expect(db).to.have.nested.property('bsonOptions.serializeFunctions'); - client.close(done); - }); + await client.close(); } - }); + ); - it('Should fail due to wrong uri user:password@localhost', { - metadata: { - requires: { topology: ['single', 'replicaset', 'sharded'] } - }, - test() { - expect(() => this.configuration.newClient('user:password@localhost:27017/test')).to.throw( - 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"' - ); - } + it('Should fail due to wrong uri user:password@localhost', function () { + expect(() => this.configuration.newClient('user:password@localhost:27017/test')).to.throw( + 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"' + ); }); - it('correctly error out when no socket available on MongoClient `connect`', { - metadata: { - requires: { topology: ['single', 'replicaset', 'sharded'] } - }, - - test: function (done) { - const configuration = this.configuration; - const client = configuration.newClient('mongodb://localhost:27088/test', { - serverSelectionTimeoutMS: 10 - }); - - client.connect(function (err) { - expect(err).to.exist; + it('correctly error out when no socket available on MongoClient `connect`', async function () { + const configuration = this.configuration; + const client = configuration.newClient('mongodb://localhost:27088/test', { + serverSelectionTimeoutMS: 10 + }); - done(); - }); - } + const error = await client.connect().catch(e => e); + expect(error).to.be.instanceOf(MongoServerSelectionError); }); it('should correctly connect to mongodb using domain socket', { metadata: { requires: { topology: ['single'], os: '!win32' } }, - - test: function (done) { + test: async function () { const configuration = this.configuration; const client = configuration.newClient('mongodb://%2Ftmp%2Fmongodb-27017.sock/test'); - client.connect(function (err) { - expect(err).to.not.exist; - client.close(done); - }); + await client.connect(); + await client.close(); } }); @@ -236,6 +217,7 @@ describe('class MongoClient', function () { beforeEach(async function () { spy = sinon.spy(net, 'createConnection'); const uri = this.configuration.url(); + // @ts-expect-error Intentional test of invalid options client = new MongoClient(uri, options); }); @@ -335,140 +317,109 @@ describe('class MongoClient', function () { }); }); - it('Should correctly pass through appname', { - metadata: { - requires: { - topology: ['single', 'replicaset', 'sharded'] - } - }, - - test: function (done) { - const configuration = this.configuration; - const options = { - appName: 'hello world' - }; - const client = configuration.newClient(options); - - client.connect(function (err, client) { - expect(err).to.not.exist; - expect(client) - .to.have.nested.property('topology.clientMetadata.application.name') - .to.equal('hello world'); - - client.close(done); - }); - } - }); - - it('Should correctly pass through appname in options', { - metadata: { - requires: { - topology: ['single', 'replicaset', 'sharded'] - } - }, - - test: function (done) { - const configuration = this.configuration; - const url = configuration.url(); - - const client = configuration.newClient(url, { appname: 'hello world' }); - client.connect(err => { - expect(err).to.not.exist; - expect(client) - .to.have.nested.property('topology.clientMetadata.application.name') - .to.equal('hello world'); + it('Should correctly pass through appname', async function () { + const configuration = this.configuration; + const options = { + appName: 'hello world' + }; + const client = configuration.newClient(options); - client.close(done); - }); - } + const { + application: { name } + } = await client.options.metadata; + expect(name).to.equal('hello world'); }); - it('Should correctly pass through socketTimeoutMS and connectTimeoutMS', { - metadata: { - requires: { - topology: ['single', 'replicaset', 'sharded'] - } - }, - - test: function (done) { - const configuration = this.configuration; - const client = configuration.newClient( - {}, - { - socketTimeoutMS: 0, - connectTimeoutMS: 0 - } - ); - - client.connect(function (err, client) { - expect(err).to.not.exist; - const topology = getTopology(client.db(configuration.db)); - expect(topology).nested.property('s.options.connectTimeoutMS').to.equal(0); - expect(topology).nested.property('s.options.socketTimeoutMS').to.equal(0); + it('Should correctly pass through appname in options', async function () { + const configuration = this.configuration; + const url = this.configuration.url(); + const client = configuration.newClient(url, { appName: 'hello world' }); - client.close(done); - }); - } + const { + application: { name } + } = await client.options.metadata; + expect(name).to.equal('hello world'); }); - it('should open a new MongoClient connection', { - metadata: { - requires: { - topology: ['single'] + it('Should correctly pass through socketTimeoutMS and connectTimeoutMS', async function () { + const configuration = this.configuration; + const client = configuration.newClient( + {}, + { + socketTimeoutMS: 0, + connectTimeoutMS: 0 } - }, + ); - test: function (done) { - const configuration = this.configuration; - const client = configuration.newClient(); - client.connect(function (err, mongoclient) { - expect(err).to.not.exist; - - mongoclient - .db('integration_tests') - .collection('new_mongo_client_collection') - .insertOne({ a: 1 }, function (err, r) { - expect(err).to.not.exist; - expect(r).to.be.an('object'); - - mongoclient.close(done); - }); - }); - } - }); + await client.connect(); + const topology = client.topology; + expect(topology).nested.property('s.options.connectTimeoutMS').to.equal(0); + expect(topology).nested.property('s.options.socketTimeoutMS').to.equal(0); - it('should correctly connect with MongoClient `connect` using Promise', function () { - const configuration = this.configuration; - let url = configuration.url(); - url = url.indexOf('?') !== -1 ? `${url}&maxPoolSize=100` : `${url}?maxPoolSize=100`; - - const client = configuration.newClient(url); - return client.connect().then(() => client.close()); + await client.close(); }); - it('should open a new MongoClient connection using promise', { - metadata: { - requires: { - topology: ['single'] - } - }, - - test: function (done) { - const configuration = this.configuration; - const client = configuration.newClient(); - client.connect().then(function (mongoclient) { - mongoclient - .db('integration_tests') - .collection('new_mongo_client_collection') - .insertOne({ a: 1 }) - .then(function (r) { - expect(r).to.exist; - - mongoclient.close(done); - }); - }); - } - }); + // TODO(NODE-7219): remove unnecessary test + // it('should open a new MongoClient connection', { + // metadata: { + // requires: { + // topology: ['single'] + // } + // }, + + // test: function (done) { + // const configuration = this.configuration; + // const client = configuration.newClient(); + // client.connect(function (err, mongoclient) { + // expect(err).to.not.exist; + + // mongoclient + // .db('integration_tests') + // .collection('new_mongo_client_collection') + // .insertOne({ a: 1 }, function (err, r) { + // expect(err).to.not.exist; + // expect(r).to.be.an('object'); + + // mongoclient.close(done); + // }); + // }); + // } + // }); + + // TODO(NODE-7219): remove unnecessary test + // it('should correctly connect with MongoClient `connect` using Promise', function () { + // const configuration = this.configuration; + // let url = configuration.url(); + // url = url.indexOf('?') !== -1 ? `${url}&maxPoolSize=100` : `${url}?maxPoolSize=100`; + + // const client = configuration.newClient(url); + // return client.connect().then(() => client.close()); + // }); + + // TODO(NODE-7219): remove unnecessary test + // it('should open a new MongoClient connection using promise', { + // metadata: { + // requires: { + // topology: ['single'] + // } + // }, + + // test: function (done) { + // const configuration = this.configuration; + // const client = configuration.newClient(); + // client.connect().then(function (mongoclient) { + // mongoclient + // .db('integration_tests') + // .collection('new_mongo_client_collection') + // .insertOne({ a: 1 }) + // .then(function (r) { + // expect(r).to.exist; + + // mongoclient.close(done); + // }); + // }); + // } + // }); it('should be able to access a database named "constructor"', function () { const client = this.configuration.newClient(); @@ -498,28 +449,18 @@ describe('class MongoClient', function () { expect(client.readPreference).to.have.property('mode', ReadPreference.SECONDARY); }); - it('should error on unexpected options', { - metadata: { requires: { topology: 'single' } }, - - test: function (done) { - const configuration = this.configuration; - MongoClient.connect( - configuration.url(), - { - maxPoolSize: 4, - // @ts-expect-error: unexpected option test - notlegal: {}, - validateOptions: true - }, - function (err, client) { - expect(err) - .property('message') - .to.match(/options notlegal, validateoptions are not supported/); - expect(client).to.not.exist; - done(); - } - ); - } + it('should error on unexpected options', async function () { + const configuration = this.configuration; + const error = await MongoClient.connect(configuration.url(), { + maxPoolSize: 4, + // @ts-expect-error: unexpected option test + notlegal: {}, + validateOptions: true + }).catch(e => e); + + expect(error) + .property('message') + .to.match(/options notlegal, validateoptions are not supported/); }); it('should error on unexpected options (promise)', { diff --git a/test/tools/cmap_spec_runner.ts b/test/tools/cmap_spec_runner.ts index 874b5043d01..28230359587 100644 --- a/test/tools/cmap_spec_runner.ts +++ b/test/tools/cmap_spec_runner.ts @@ -323,7 +323,7 @@ export class ThreadContext { serverApi: process.env.MONGODB_API_VERSION ? { version: process.env.MONGODB_API_VERSION } : undefined, - extendedMetadata: this.#server.topology.client.options.extendedMetadata + metadata: this.#server.topology.client.options.metadata }); this.#originalServerPool = this.#server.pool; this.#server.pool = this.pool; diff --git a/test/tools/uri_spec_runner.ts b/test/tools/uri_spec_runner.ts index 8fc68b37ae9..bb6f44459a1 100644 --- a/test/tools/uri_spec_runner.ts +++ b/test/tools/uri_spec_runner.ts @@ -329,7 +329,7 @@ export function executeUriValidationTest( //** MISC SPECIAL PARSE RULE OPTIONS **/ case 'appname': - expectedProp = 'metadata.application.name'; + expectedProp = 'appName'; expect(options, `${errorMessage} ${optionKey} -> ${expectedProp}`) .to.have.nested.property(expectedProp) .equal(optionValue); diff --git a/test/unit/cmap/connect.test.ts b/test/unit/cmap/connect.test.ts index ba54946f589..a3e8f7dac36 100644 --- a/test/unit/cmap/connect.test.ts +++ b/test/unit/cmap/connect.test.ts @@ -1,12 +1,11 @@ -import { type Document } from 'bson'; import { expect } from 'chai'; import { MongoCredentials } from '../../../src/cmap/auth/mongo_credentials'; import { connect, prepareHandshakeDocument } from '../../../src/cmap/connect'; import { type Connection, type ConnectionOptions } from '../../../src/cmap/connection'; import { - addContainerMetadata, - type ClientMetadata + type ClientMetadata, + makeClientMetadata } from '../../../src/cmap/handshake/client_metadata'; import { LEGACY_HELLO_COMMAND } from '../../../src/constants'; import { MongoNetworkError } from '../../../src/error'; @@ -21,11 +20,22 @@ const CONNECT_DEFAULTS = { tls: false, generation: 1, monitorCommands: false, - metadata: {} as ClientMetadata, - extendedMetadata: addContainerMetadata({} as ClientMetadata), + metadata: Promise.resolve({} as ClientMetadata), loadBalanced: false }; +function configureMockEnvHooks(env: NodeJS.ProcessEnv) { + const cachedEnv = process.env; + + beforeEach(function () { + process.env = env; + }); + + afterEach(function () { + process.env = cachedEnv; + }); +} + describe('Connect Tests', function () { context('when PLAIN auth enabled', () => { const test: { @@ -187,28 +197,24 @@ describe('Connect Tests', function () { describe('prepareHandshakeDocument', () => { describe('client environment (containers and FAAS)', () => { - const cachedEnv = process.env; - context('when only kubernetes is present', () => { let authContext; + configureMockEnvHooks({ + KUBERNETES_SERVICE_HOST: 'I exist' + }); + beforeEach(() => { - process.env.KUBERNETES_SERVICE_HOST = 'I exist'; authContext = { connection: {}, options: { ...CONNECT_DEFAULTS, - extendedMetadata: addContainerMetadata({} as ClientMetadata) + metadata: makeClientMetadata([], {}) } }; }); afterEach(() => { - if (cachedEnv.KUBERNETES_SERVICE_HOST != null) { - process.env.KUBERNETES_SERVICE_HOST = cachedEnv.KUBERNETES_SERVICE_HOST; - } else { - delete process.env.KUBERNETES_SERVICE_HOST; - } authContext = {}; }); @@ -224,13 +230,21 @@ describe('Connect Tests', function () { context('when 512 byte size limit is exceeded', () => { it(`should not 'env' property in client`, async () => { - // make metadata = 507 bytes, so it takes up entire LimitedSizeDocument + // make a metadata object that, with just the name and appName, is already at capacity. const longAppName = 's'.repeat(493); + const metadata = makeClientMetadata( + [ + { + name: 's'.repeat(128) + } + ], + { appName: longAppName } + ); const longAuthContext = { connection: {}, options: { ...CONNECT_DEFAULTS, - extendedMetadata: addContainerMetadata({ appName: longAppName }) + metadata } }; const handshakeDocument = await prepareHandshakeDocument(longAuthContext); @@ -242,23 +256,22 @@ describe('Connect Tests', function () { context('when kubernetes and FAAS are both present', () => { let authContext; + configureMockEnvHooks({ + KUBERNETES_SERVICE_HOST: 'I exist', + AWS_EXECUTION_ENV: 'AWS_Lambda_function' + }); + beforeEach(() => { - process.env.KUBERNETES_SERVICE_HOST = 'I exist'; authContext = { connection: {}, options: { ...CONNECT_DEFAULTS, - extendedMetadata: addContainerMetadata({ env: { name: 'aws.lambda' } }) + metadata: makeClientMetadata([], {}) } }; }); afterEach(() => { - if (cachedEnv.KUBERNETES_SERVICE_HOST != null) { - process.env.KUBERNETES_SERVICE_HOST = cachedEnv.KUBERNETES_SERVICE_HOST; - } else { - delete process.env.KUBERNETES_SERVICE_HOST; - } authContext = {}; }); @@ -274,16 +287,21 @@ describe('Connect Tests', function () { context('when 512 byte size limit is exceeded', () => { it(`should not have 'container' property in client.env`, async () => { - // make metadata = 507 bytes, so it takes up entire LimitedSizeDocument const longAppName = 's'.repeat(447); + // make a metadata object that, with just the name and appName, is already at capacity. + const metadata = makeClientMetadata( + [ + { + name: 's'.repeat(128) + } + ], + { appName: longAppName } + ); const longAuthContext = { connection: {}, options: { ...CONNECT_DEFAULTS, - extendedMetadata: { - appName: longAppName, - env: { name: 'aws.lambda' } - } as unknown as Promise + metadata } }; const handshakeDocument = await prepareHandshakeDocument(longAuthContext); @@ -300,19 +318,7 @@ describe('Connect Tests', function () { }; context('when process.env.KUBERNETES_SERVICE_HOST = undefined', () => { - beforeEach(() => { - delete process.env.KUBERNETES_SERVICE_HOST; - }); - - afterEach(() => { - afterEach(() => { - if (cachedEnv.KUBERNETES_SERVICE_HOST != null) { - process.env.KUBERNETES_SERVICE_HOST = cachedEnv.KUBERNETES_SERVICE_HOST; - } else { - delete process.env.KUBERNETES_SERVICE_HOST; - } - }); - }); + configureMockEnvHooks({ KUBERNETES_SERVICE_HOST: undefined }); it(`should not have 'env' property in client`, async () => { const handshakeDocument = await prepareHandshakeDocument(authContext); @@ -321,16 +327,8 @@ describe('Connect Tests', function () { }); context('when process.env.KUBERNETES_SERVICE_HOST is an empty string', () => { - beforeEach(() => { - process.env.KUBERNETES_SERVICE_HOST = ''; - }); - - afterEach(() => { - if (cachedEnv.KUBERNETES_SERVICE_HOST != null) { - process.env.KUBERNETES_SERVICE_HOST = cachedEnv.KUBERNETES_SERVICE_HOST; - } else { - delete process.env.KUBERNETES_SERVICE_HOST; - } + configureMockEnvHooks({ + KUBERNETES_SERVICE_HOST: '' }); it(`should not have 'env' property in client`, async () => { diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 09c5a06d1cd..87069dacc30 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -140,8 +140,8 @@ describe('client metadata module', () => { describe('makeClientMetadata()', () => { context('when no FAAS environment is detected', () => { - it('does not append FAAS metadata', () => { - const metadata = makeClientMetadata([], {}); + it('does not append FAAS metadata', async () => { + const metadata = await makeClientMetadata([], {}); expect(metadata).not.to.have.property( 'env', 'faas metadata applied in a non-faas environment' @@ -163,15 +163,15 @@ describe('client metadata module', () => { }); context('when driverInfo.platform is provided', () => { - it('throws an error if driverInfo.platform is too large', () => { - expect(() => makeClientMetadata([{ platform: 'a'.repeat(512) }], {})).to.throw( - MongoInvalidArgumentError, - /platform/ - ); + it('throws an error if driverInfo.platform is too large', async () => { + const error = await makeClientMetadata([{ platform: 'a'.repeat(512) }], {}).catch(e => e); + expect(error) + .to.be.instanceOf(MongoInvalidArgumentError) + .to.match(/platform/); }); - it('appends driverInfo.platform to the platform field', () => { - const metadata = makeClientMetadata([{ platform: 'myPlatform' }], {}); + it('appends driverInfo.platform to the platform field', async () => { + const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], {}); expect(metadata).to.deep.equal({ driver: { name: 'nodejs', @@ -189,15 +189,13 @@ describe('client metadata module', () => { }); context('when driverInfo.name is provided', () => { - it('throws an error if driverInfo.name is too large', () => { - expect(() => makeClientMetadata([{ name: 'a'.repeat(512) }], {})).to.throw( - MongoInvalidArgumentError, - /name/ - ); + it('throws an error if driverInfo.name is too large', async () => { + const error = await makeClientMetadata([{ name: 'a'.repeat(512) }], {}).catch(e => e); + expect(error).to.be.instanceOf(MongoInvalidArgumentError).to.match(/name/); }); - it('appends driverInfo.name to the driver.name field', () => { - const metadata = makeClientMetadata([{ name: 'myName' }], {}); + it('appends driverInfo.name to the driver.name field', async () => { + const metadata = await makeClientMetadata([{ name: 'myName' }], {}); expect(metadata).to.deep.equal({ driver: { name: 'nodejs|myName', @@ -215,15 +213,15 @@ describe('client metadata module', () => { }); context('when driverInfo.version is provided', () => { - it('throws an error if driverInfo.version is too large', () => { - expect(() => makeClientMetadata([{ version: 'a'.repeat(512) }], {})).to.throw( - MongoInvalidArgumentError, - /version/ - ); + it('throws an error if driverInfo.version is too large', async () => { + const error = await makeClientMetadata([{ version: 'a'.repeat(512) }], {}).catch(e => e); + expect(error) + .to.be.instanceOf(MongoInvalidArgumentError) + .to.match(/version/); }); - it('appends driverInfo.version to the version field', () => { - const metadata = makeClientMetadata([{ version: 'myVersion' }], {}); + it('appends driverInfo.version to the version field', async () => { + const metadata = await makeClientMetadata([{ version: 'myVersion' }], {}); expect(metadata).to.deep.equal({ driver: { name: 'nodejs', @@ -241,9 +239,8 @@ describe('client metadata module', () => { }); context('when no custom driverInto is provided', () => { - const metadata = makeClientMetadata([], {}); - - it('does not append the driver info to the metadata', () => { + it('does not append the driver info to the metadata', async () => { + const metadata = await makeClientMetadata([], {}); expect(metadata).to.deep.equal({ driver: { name: 'nodejs', @@ -259,19 +256,19 @@ describe('client metadata module', () => { }); }); - it('does not set the application field', () => { + it('does not set the application field', async () => { + const metadata = await makeClientMetadata([], {}); expect(metadata).not.to.have.property('application'); }); }); context('when app name is provided', () => { context('when the app name is over 128 bytes', () => { - const longString = 'a'.repeat(300); - const metadata = makeClientMetadata([], { - appName: longString - }); - - it('truncates the application name to <=128 bytes', () => { + it('truncates the application name to <=128 bytes', async () => { + const longString = 'a'.repeat(300); + const metadata = await makeClientMetadata([], { + appName: longString + }); expect(metadata.application?.name).to.be.a('string'); // the above assertion fails if `metadata.application?.name` is undefined, so // we can safely assert that it exists @@ -283,12 +280,12 @@ describe('client metadata module', () => { context( 'TODO(NODE-5150): fix appName truncation when multi-byte unicode charaters straddle byte 128', () => { - const longString = '€'.repeat(300); - const metadata = makeClientMetadata([], { - appName: longString - }); + it('truncates the application name to 129 bytes', async () => { + const longString = '€'.repeat(300); + const metadata = await makeClientMetadata([], { + appName: longString + }); - it('truncates the application name to 129 bytes', () => { expect(metadata.application?.name).to.be.a('string'); // the above assertion fails if `metadata.application?.name` is undefined, so // we can safely assert that it exists @@ -299,11 +296,10 @@ describe('client metadata module', () => { ); context('when the app name is under 128 bytes', () => { - const metadata = makeClientMetadata([], { - appName: 'myApplication' - }); - - it('sets the application name to the value', () => { + it('sets the application name to the value', async () => { + const metadata = await makeClientMetadata([], { + appName: 'myApplication' + }); expect(metadata.application?.name).to.equal('myApplication'); }); }); @@ -315,39 +311,39 @@ describe('client metadata module', () => { expect(delete globalThis.Deno, 'failed to delete Deno global').to.be.true; }); - it('sets platform to Deno', () => { + it('sets platform to Deno', async () => { globalThis.Deno = { version: { deno: '1.2.3' } }; - const metadata = makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], {}); expect(metadata.platform).to.equal('Deno v1.2.3, LE'); }); - it('sets platform to Deno with driverInfo.platform', () => { + it('sets platform to Deno with driverInfo.platform', async () => { globalThis.Deno = { version: { deno: '1.2.3' } }; - const metadata = makeClientMetadata([{ platform: 'myPlatform' }], {}); + const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], {}); expect(metadata.platform).to.equal('Deno v1.2.3, LE|myPlatform'); }); - it('ignores version if Deno.version.deno is not a string', () => { + it('ignores version if Deno.version.deno is not a string', async () => { globalThis.Deno = { version: { deno: 1 } }; - const metadata = makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], {}); expect(metadata.platform).to.equal('Deno v0.0.0-unknown, LE'); }); - it('ignores version if Deno.version does not have a deno property', () => { + it('ignores version if Deno.version does not have a deno property', async () => { globalThis.Deno = { version: { somethingElse: '1.2.3' } }; - const metadata = makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], {}); expect(metadata.platform).to.equal('Deno v0.0.0-unknown, LE'); }); - it('ignores version if Deno.version is null', () => { + it('ignores version if Deno.version is null', async () => { globalThis.Deno = { version: null }; - const metadata = makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], {}); expect(metadata.platform).to.equal('Deno v0.0.0-unknown, LE'); }); - it('ignores version if Deno is nullish', () => { + it('ignores version if Deno is nullish', async () => { globalThis.Deno = null; - const metadata = makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], {}); expect(metadata.platform).to.equal('Deno v0.0.0-unknown, LE'); }); }); @@ -357,41 +353,41 @@ describe('client metadata module', () => { expect(delete globalThis.Bun, 'failed to delete Bun global').to.be.true; }); - it('sets platform to Bun', () => { + it('sets platform to Bun', async () => { globalThis.Bun = class { static version = '1.2.3'; }; - const metadata = makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], {}); expect(metadata.platform).to.equal('Bun v1.2.3, LE'); }); - it('sets platform to Bun with driverInfo.platform', () => { + it('sets platform to Bun with driverInfo.platform', async () => { globalThis.Bun = class { static version = '1.2.3'; }; - const metadata = makeClientMetadata([{ platform: 'myPlatform' }], {}); + const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], {}); expect(metadata.platform).to.equal('Bun v1.2.3, LE|myPlatform'); }); - it('ignores version if Bun.version is not a string', () => { + it('ignores version if Bun.version is not a string', async () => { globalThis.Bun = class { static version = 1; }; - const metadata = makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], {}); expect(metadata.platform).to.equal('Bun v0.0.0-unknown, LE'); }); - it('ignores version if Bun.version is not a string and sets driverInfo.platform', () => { + it('ignores version if Bun.version is not a string and sets driverInfo.platform', async () => { globalThis.Bun = class { static version = 1; }; - const metadata = makeClientMetadata([{ platform: 'myPlatform' }], {}); + const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], {}); expect(metadata.platform).to.equal('Bun v0.0.0-unknown, LE|myPlatform'); }); - it('ignores version if Bun is nullish', () => { + it('ignores version if Bun is nullish', async () => { globalThis.Bun = null; - const metadata = makeClientMetadata([{ platform: 'myPlatform' }], {}); + const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], {}); expect(metadata.platform).to.equal('Bun v0.0.0-unknown, LE|myPlatform'); }); }); @@ -511,8 +507,8 @@ describe('client metadata module', () => { sinon.stub(process, 'env').get(() => Object.fromEntries(faasVariables)); }); - it(`returns ${inspect(outcome)} under env property`, () => { - const { env } = makeClientMetadata([], {}); + it(`returns ${inspect(outcome)} under env property`, async () => { + const { env } = await makeClientMetadata([], {}); expect(env).to.deep.equal(outcome); }); @@ -535,8 +531,8 @@ describe('client metadata module', () => { delete process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE; }); - it('does not attach it to the metadata', () => { - expect(makeClientMetadata([], {})).not.to.have.nested.property('aws.memory_mb'); + it('does not attach it to the metadata', async () => { + expect(await makeClientMetadata([], {})).not.to.have.nested.property('aws.memory_mb'); }); }); }); @@ -550,8 +546,8 @@ describe('client metadata module', () => { })); }); - it('only includes env.name', () => { - const metadata = makeClientMetadata([], {}); + it('only includes env.name', async () => { + const metadata = await makeClientMetadata([], {}); expect(metadata).to.not.have.nested.property('env.region'); expect(metadata).to.have.nested.property('env.name', 'aws.lambda'); expect(metadata.env).to.have.all.keys('name'); @@ -568,8 +564,8 @@ describe('client metadata module', () => { sinon.stub(os, 'release').returns('a'.repeat(512)); }); - it('only includes env.name', () => { - const metadata = makeClientMetadata([], {}); + it('only includes env.name', async () => { + const metadata = await makeClientMetadata([], {}); expect(metadata).to.have.property('env'); expect(metadata).to.have.nested.property('env.region', 'abc'); expect(metadata.os).to.have.all.keys('type'); @@ -585,8 +581,8 @@ describe('client metadata module', () => { sinon.stub(os, 'type').returns('a'.repeat(512)); }); - it('omits os information', () => { - const metadata = makeClientMetadata([], {}); + it('omits os information', async () => { + const metadata = await makeClientMetadata([], {}); expect(metadata).to.not.have.property('os'); }); }); @@ -601,8 +597,8 @@ describe('client metadata module', () => { sinon.stub(os, 'type').returns('a'.repeat(50)); }); - it('omits the faas env', () => { - const metadata = makeClientMetadata([{ name: 'a'.repeat(350) }], {}); + it('omits the faas env', async () => { + const metadata = await makeClientMetadata([{ name: 'a'.repeat(350) }], {}); expect(metadata).to.not.have.property('env'); }); }); diff --git a/test/unit/sdam/topology.test.ts b/test/unit/sdam/topology.test.ts index 8fac7f52ee9..67d245e687a 100644 --- a/test/unit/sdam/topology.test.ts +++ b/test/unit/sdam/topology.test.ts @@ -51,14 +51,15 @@ describe('Topology (unit)', function () { after(() => mock.cleanup()); - it('should correctly pass appname', function () { - const server: Topology = topologyWithPlaceholderClient([`localhost:27017`], { + it('should correctly pass appname', async function () { + const topology: Topology = topologyWithPlaceholderClient([`localhost:27017`], { metadata: makeClientMetadata([], { appName: 'My application name' }) }); - expect(server.clientMetadata?.application.name).to.equal('My application name'); + const metadata = await topology.s.options.metadata; + expect(metadata.application.name).to.equal('My application name'); }); it('should report the correct platform in client metadata', async function () {