From a317cce189c367ef2c1f47451401c30323db5c3b Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 9 Nov 2025 19:54:22 -0600 Subject: [PATCH 01/22] Add additional options to MongoCluster --- packages/mongodb-runner/src/mongocluster.ts | 92 ++++++++++++++------- 1 file changed, 60 insertions(+), 32 deletions(-) diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index 9e75119c..bf8f2fd9 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -22,6 +22,10 @@ export interface MongoClusterOptions downloadDir?: string; downloadOptions?: DownloadOptions; oidc?: string; + rsTags?: Map[]; + rsArgs?: string[][]; + shardArgs?: string[][]; + mongosArgs?: string[][]; } export type MongoClusterEvents = { @@ -186,6 +190,9 @@ export class MongoCluster extends EventEmitter { } const primaryArgs = [...args]; + if (options.rsArgs?.length > 0) { + primaryArgs.push(...options.rsArgs[0]) + } debug('Starting primary', primaryArgs); const primary = await MongoServer.start({ ...options, @@ -201,17 +208,22 @@ export class MongoCluster extends EventEmitter { debug('Starting secondaries and arbiters', { secondaries, arbiters, - args, + args }); cluster.servers.push( ...(await Promise.all( - range(secondaries + arbiters).map(() => - MongoServer.start({ + range(secondaries + arbiters).map((i) => { + const secondaryArgs = [...args]; + if (i < options.rsArgs?.length) { + secondaryArgs.push(...options.rsArgs[i]); + debug('Adding secondary args', options.rsArgs[i]) + } + return MongoServer.start({ ...options, - args, + args: secondaryArgs, binary: 'mongod', - }), - ), + }); + }) )), ); @@ -221,11 +233,13 @@ export class MongoCluster extends EventEmitter { _id: replSetName, configsvr: args.includes('--configsvr'), members: cluster.servers.map((srv, i) => { + const tags = (i < options.rsTags.length) ? options.rsTags[i] : {}; return { _id: i, host: srv.hostport, arbiterOnly: i > 1 + secondaries, priority: i === 0 ? 1 : 0, + tags }; }), }; @@ -257,37 +271,51 @@ export class MongoCluster extends EventEmitter { debug('starting config server and shard servers', shardArgs); const [configsvr, ...shardsvrs] = await Promise.all( - range(shards + 1).map((i) => - MongoCluster.start({ + range(shards + 1).map((i) => { + const args: string[] = [...shardArgs]; + if (i == 0) { + args.push('--configsvr'); + } else { + args.push('--shardsvr'); + if (i - 1 < options.shardArgs?.length) { + args.push(...options.shardArgs[i - 1]); + debug('Adding shard args', options.shardArgs[i - 1]); + } + } + return MongoCluster.start({ ...options, - args: [...shardArgs, i > 0 ? '--shardsvr' : '--configsvr'], + args, topology: 'replset', - }), - ), + }); + }), ); cluster.shards.push(configsvr, ...shardsvrs); - debug('starting mongos'); - const mongos = await MongoServer.start({ - ...options, - binary: 'mongos', - args: [ - ...(options.args ?? []), - '--configdb', - `${configsvr.replSetName!}/${configsvr.hostport}`, - ], - }); - cluster.servers.push(mongos); - await mongos.withClient(async (client) => { - for (const shard of shardsvrs) { - const shardSpec = `${shard.replSetName!}/${shard.hostport}`; - debug('adding shard', shardSpec); - await client.db('admin').command({ - addShard: shardSpec, - }); - } - debug('added shards'); - }); + const mongosArgs = options.mongosArgs ?? [[]]; + for (let i = 0; i < mongosArgs.length; i++) { + debug('starting mongos'); + const mongos = await MongoServer.start({ + ...options, + binary: 'mongos', + args: [ + ...(options.args ?? []), + ...mongosArgs[i], + '--configdb', + `${configsvr.replSetName!}/${configsvr.hostport}`, + ], + }); + cluster.servers.push(mongos); + await mongos.withClient(async (client) => { + for (const shard of shardsvrs) { + const shardSpec = `${shard.replSetName!}/${shard.hostport}`; + debug('adding shard', shardSpec); + await client.db('admin').command({ + addShard: shardSpec, + }); + } + debug('added shards'); + }); + } } return cluster; } From b6beb6098971f03ba6f8b4428adb9eb4e11f4e9a Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 10 Nov 2025 15:51:13 -0600 Subject: [PATCH 02/22] wip add auth support --- packages/mongodb-runner/src/mongocluster.ts | 50 +++++++++++++------ packages/mongodb-runner/src/mongoserver.ts | 55 +++++++++++++++++++-- 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index bf8f2fd9..27b65db0 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -12,7 +12,7 @@ import { EventEmitter } from 'events'; export interface MongoClusterOptions extends Pick< MongoServerOptions, - 'logDir' | 'tmpDir' | 'args' | 'binDir' | 'docker' + 'logDir' | 'tmpDir' | 'args' | 'binDir' | 'docker' | 'login' | 'password' > { topology: 'standalone' | 'replset' | 'sharded'; arbiters?: number; @@ -22,10 +22,11 @@ export interface MongoClusterOptions downloadDir?: string; downloadOptions?: DownloadOptions; oidc?: string; - rsTags?: Map[]; + rsTags?: Map[]; rsArgs?: string[][]; shardArgs?: string[][]; mongosArgs?: string[][]; + roles?: Map[]; } export type MongoClusterEvents = { @@ -177,6 +178,10 @@ export class MongoCluster extends EventEmitter { binary: 'mongod', }), ); + if (options.login) { + await cluster.servers[0].addAdminUser(options.roles); + await cluster.servers[0].reinitialize(); + } } else if (options.topology === 'replset') { const { secondaries = 2, arbiters = 0 } = options; @@ -190,8 +195,9 @@ export class MongoCluster extends EventEmitter { } const primaryArgs = [...args]; - if (options.rsArgs?.length > 0) { - primaryArgs.push(...options.rsArgs[0]) + const rsArgs = options.rsArgs || [[]]; + if (rsArgs.length > 0) { + primaryArgs.push(...rsArgs[0]); } debug('Starting primary', primaryArgs); const primary = await MongoServer.start({ @@ -208,38 +214,39 @@ export class MongoCluster extends EventEmitter { debug('Starting secondaries and arbiters', { secondaries, arbiters, - args + args, }); cluster.servers.push( ...(await Promise.all( range(secondaries + arbiters).map((i) => { const secondaryArgs = [...args]; - if (i < options.rsArgs?.length) { - secondaryArgs.push(...options.rsArgs[i]); - debug('Adding secondary args', options.rsArgs[i]) + if (i + 1 < rsArgs.length) { + secondaryArgs.push(...rsArgs[i + 1]); + debug('Adding secondary args', rsArgs[i + 1]); } return MongoServer.start({ ...options, args: secondaryArgs, binary: 'mongod', }); - }) + }), )), ); await primary.withClient(async (client) => { debug('Running rs.initiate'); + const rsTags = options.rsTags || [{}]; const rsConf = { _id: replSetName, configsvr: args.includes('--configsvr'), members: cluster.servers.map((srv, i) => { - const tags = (i < options.rsTags.length) ? options.rsTags[i] : {}; + const tags = i < rsTags.length || 0 ? rsTags[i] : {}; return { _id: i, host: srv.hostport, arbiterOnly: i > 1 + secondaries, priority: i === 0 ? 1 : 0, - tags + tags, }; }), }; @@ -261,6 +268,14 @@ export class MongoCluster extends EventEmitter { debug('rs.status did not include primary, waiting...'); await sleep(1000); } + + // Add auth if needed + if (options.login) { + await cluster.servers[0].addAdminUser(options.roles); + for (const server of cluster.servers) { + await server.reinitialize(); + } + } }); } else if (options.topology === 'sharded') { const { shards = 3 } = options; @@ -268,18 +283,19 @@ export class MongoCluster extends EventEmitter { if (shardArgs.includes('--port')) { shardArgs.splice(shardArgs.indexOf('--port') + 1, 1, '0'); } + const perShardArgs = options.shardArgs || [[]]; debug('starting config server and shard servers', shardArgs); const [configsvr, ...shardsvrs] = await Promise.all( range(shards + 1).map((i) => { const args: string[] = [...shardArgs]; - if (i == 0) { + if (i === 0) { args.push('--configsvr'); } else { args.push('--shardsvr'); - if (i - 1 < options.shardArgs?.length) { - args.push(...options.shardArgs[i - 1]); - debug('Adding shard args', options.shardArgs[i - 1]); + if (i - 1 < perShardArgs.length) { + args.push(...perShardArgs[i - 1]); + debug('Adding shard args', perShardArgs[i - 1]); } } return MongoCluster.start({ @@ -304,6 +320,10 @@ export class MongoCluster extends EventEmitter { `${configsvr.replSetName!}/${configsvr.hostport}`, ], }); + if (options.login) { + mongos.addAdminUser(options.roles); + mongos.reinitialize(); + } cluster.servers.push(mongos); await mongos.withClient(async (client) => { for (const shard of shardsvrs) { diff --git a/packages/mongodb-runner/src/mongoserver.ts b/packages/mongodb-runner/src/mongoserver.ts index 2f7b0643..b2385b95 100644 --- a/packages/mongodb-runner/src/mongoserver.ts +++ b/packages/mongodb-runner/src/mongoserver.ts @@ -22,6 +22,8 @@ export interface MongoServerOptions { logDir?: string; // If set, pipe log file output through here. args?: string[]; // May or may not contain --port docker?: string | string[]; // Image or docker options + login?: string; // Simple user login. + password?: string; // Simple user password. } interface SerializedServerProperties { @@ -31,6 +33,7 @@ interface SerializedServerProperties { dbPath?: string; startTime: string; hasInsertedMetadataCollEntry: boolean; + hasAuth: boolean; } export interface MongoServerEvents { @@ -47,6 +50,9 @@ export class MongoServer extends EventEmitter { private closing = false; private startTime = new Date().toISOString(); private hasInsertedMetadataCollEntry = false; + private login?: string; + private password?: string; + private addAuth?: boolean; get id(): string { return this.uuid; @@ -65,6 +71,7 @@ export class MongoServer extends EventEmitter { dbPath: this.dbPath, startTime: this.startTime, hasInsertedMetadataCollEntry: this.hasInsertedMetadataCollEntry, + hasAuth: this.login ? this.login.length > 0 : false, }; } @@ -74,7 +81,8 @@ export class MongoServer extends EventEmitter { const srv = new MongoServer(); srv.uuid = serialized._id; srv.port = serialized.port; - srv.closing = !!(await srv._populateBuildInfo('restore-check')); + srv.closing = + serialized.hasAuth || !!(await srv._populateBuildInfo('restore-check')); if (!srv.closing) { srv.pid = serialized.pid; srv.dbPath = serialized.dbPath; @@ -264,9 +272,14 @@ export class MongoServer extends EventEmitter { logEntryStream.resume(); srv.port = port; - const buildInfoError = await srv._populateBuildInfo('insert-new'); - if (buildInfoError) { - debug('failed to get buildInfo', buildInfoError); + if (options.login === undefined) { + const buildInfoError = await srv._populateBuildInfo('insert-new'); + if (buildInfoError) { + debug('failed to get buildInfo', buildInfoError); + } + } else { + srv.login = options.login; + srv.password = options.password; } } catch (err) { await srv.close(); @@ -276,6 +289,30 @@ export class MongoServer extends EventEmitter { return srv; } + async addAdminUser(roles?: Map[]) { + debug('Adding admin user', this.hostport); + const client = await MongoClient.connect(`mongodb://${this.hostport}/`, { + directConnection: true, + }); + try { + await client + .db('admin') + .command({ createUser: this.login, pwd: this.password, roles }); + } finally { + await client.close(); + } + } + + async reinitialize() { + if (this.login) { + this.addAuth = true; + } + const buildInfoError = await this._populateBuildInfo('insert-new'); + if (buildInfoError) { + debug('failed to get buildInfo', buildInfoError); + } + } + async close(): Promise { this.closing = true; if (this.childProcess) { @@ -315,6 +352,7 @@ export class MongoServer extends EventEmitter { 'port', 'dbPath', 'startTime', + 'hasAuth', ]); const runnerColl = client .db(isMongoS ? 'config' : 'local') @@ -376,7 +414,14 @@ export class MongoServer extends EventEmitter { fn: Fn, clientOptions: MongoClientOptions = {}, ): Promise> { - const client = await MongoClient.connect(`mongodb://${this.hostport}/`, { + let url: string; + if (this.addAuth) { + debug('ADDING LOGIN'); + url = `mongodb://${this.login}:${this.password}@${this.hostport}/`; + } else { + url = `mongodb://${this.hostport}/`; + } + const client = await MongoClient.connect(url, { directConnection: true, ...clientOptions, }); From bde597f613d06da6ebcbcf09a920d927bbfede3b Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 10 Nov 2025 21:23:41 -0600 Subject: [PATCH 03/22] finish support for auth and ssl --- packages/mongodb-runner/src/mongocluster.ts | 21 +++++++++++++--- packages/mongodb-runner/src/mongoserver.ts | 27 ++++++++++++--------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index 27b65db0..3761f8e3 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -12,7 +12,14 @@ import { EventEmitter } from 'events'; export interface MongoClusterOptions extends Pick< MongoServerOptions, - 'logDir' | 'tmpDir' | 'args' | 'binDir' | 'docker' | 'login' | 'password' + | 'logDir' + | 'tmpDir' + | 'args' + | 'binDir' + | 'docker' + | 'login' + | 'password' + | 'clientOptions' > { topology: 'standalone' | 'replset' | 'sharded'; arbiters?: number; @@ -22,11 +29,11 @@ export interface MongoClusterOptions downloadDir?: string; downloadOptions?: DownloadOptions; oidc?: string; - rsTags?: Map[]; + rsTags?: { [key: string]: string }[]; rsArgs?: string[][]; shardArgs?: string[][]; mongosArgs?: string[][]; - roles?: Map[]; + roles?: { [key: string]: string }[]; } export type MongoClusterEvents = { @@ -261,7 +268,11 @@ export class MongoCluster extends EventEmitter { if ( status.members.some((member: any) => member.stateStr === 'PRIMARY') ) { - debug('rs.status indicated primary for replset', status.set); + debug( + 'rs.status indicated primary for replset', + status.set, + status.members, + ); cluster.replSetName = status.set; break; } @@ -271,6 +282,8 @@ export class MongoCluster extends EventEmitter { // Add auth if needed if (options.login) { + // Sleep to give time for the election to settle. + await sleep(1000); await cluster.servers[0].addAdminUser(options.roles); for (const server of cluster.servers) { await server.reinitialize(); diff --git a/packages/mongodb-runner/src/mongoserver.ts b/packages/mongodb-runner/src/mongoserver.ts index b2385b95..c42f9d5d 100644 --- a/packages/mongodb-runner/src/mongoserver.ts +++ b/packages/mongodb-runner/src/mongoserver.ts @@ -24,6 +24,7 @@ export interface MongoServerOptions { docker?: string | string[]; // Image or docker options login?: string; // Simple user login. password?: string; // Simple user password. + clientOptions?: { [key: string]: string }; // Extra options to pass to clients. } interface SerializedServerProperties { @@ -53,6 +54,7 @@ export class MongoServer extends EventEmitter { private login?: string; private password?: string; private addAuth?: boolean; + private clientOptions?: { [key: string]: string }; get id(): string { return this.uuid; @@ -81,8 +83,11 @@ export class MongoServer extends EventEmitter { const srv = new MongoServer(); srv.uuid = serialized._id; srv.port = serialized.port; - srv.closing = - serialized.hasAuth || !!(await srv._populateBuildInfo('restore-check')); + if (!serialized.hasAuth) { + srv.closing = !!(await srv._populateBuildInfo('restore-check')); + } else { + srv.closing = false; + } if (!srv.closing) { srv.pid = serialized.pid; srv.dbPath = serialized.dbPath; @@ -151,6 +156,7 @@ export class MongoServer extends EventEmitter { ...options }: MongoServerOptions): Promise { const srv = new MongoServer(); + srv.clientOptions = options.clientOptions || {}; if (!options.docker) { const dbPath = path.join(options.tmpDir, `db-${srv.uuid}`); @@ -289,21 +295,17 @@ export class MongoServer extends EventEmitter { return srv; } - async addAdminUser(roles?: Map[]) { - debug('Adding admin user', this.hostport); - const client = await MongoClient.connect(`mongodb://${this.hostport}/`, { - directConnection: true, - }); - try { + async addAdminUser(roles?: { [key: string]: string }[]) { + debug('adding admin user', this.hostport); + await this.withClient(async (client) => { await client .db('admin') .command({ createUser: this.login, pwd: this.password, roles }); - } finally { - await client.close(); - } + }); } async reinitialize() { + debug('reinitializing', this.hostport); if (this.login) { this.addAuth = true; } @@ -315,6 +317,7 @@ export class MongoServer extends EventEmitter { async close(): Promise { this.closing = true; + debug('in close', 'pid', this.pid); if (this.childProcess) { debug('closing running process', this.childProcess.pid); if ( @@ -416,13 +419,13 @@ export class MongoServer extends EventEmitter { ): Promise> { let url: string; if (this.addAuth) { - debug('ADDING LOGIN'); url = `mongodb://${this.login}:${this.password}@${this.hostport}/`; } else { url = `mongodb://${this.hostport}/`; } const client = await MongoClient.connect(url, { directConnection: true, + ...this.clientOptions, ...clientOptions, }); try { From 462f151c5a9b0fc9e07ca81b3652e43a2892a000 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 11 Nov 2025 07:22:38 -0600 Subject: [PATCH 04/22] finish sharded support --- packages/mongodb-runner/src/mongocluster.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index 3761f8e3..871fe277 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -305,11 +305,13 @@ export class MongoCluster extends EventEmitter { if (i === 0) { args.push('--configsvr'); } else { - args.push('--shardsvr'); if (i - 1 < perShardArgs.length) { args.push(...perShardArgs[i - 1]); debug('Adding shard args', perShardArgs[i - 1]); } + if (!args.includes('--shardsvr')) { + args.push('--shardsvr'); + } } return MongoCluster.start({ ...options, @@ -333,11 +335,8 @@ export class MongoCluster extends EventEmitter { `${configsvr.replSetName!}/${configsvr.hostport}`, ], }); - if (options.login) { - mongos.addAdminUser(options.roles); - mongos.reinitialize(); - } cluster.servers.push(mongos); + await mongos.reinitialize(); await mongos.withClient(async (client) => { for (const shard of shardsvrs) { const shardSpec = `${shard.replSetName!}/${shard.hostport}`; From 28a10e2f4dad4ca6b1f4ec92c32b2800d061a4df Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 14 Nov 2025 16:36:05 +0100 Subject: [PATCH 05/22] align more with mongodb-runner design principles --- packages/mongodb-runner/src/index.ts | 1 + packages/mongodb-runner/src/mongocluster.ts | 103 +++++++++++++------- packages/mongodb-runner/src/mongoserver.ts | 78 ++++++--------- packages/mongodb-runner/src/util.ts | 4 + 4 files changed, 100 insertions(+), 86 deletions(-) diff --git a/packages/mongodb-runner/src/index.ts b/packages/mongodb-runner/src/index.ts index 1bc50d23..49037e74 100644 --- a/packages/mongodb-runner/src/index.ts +++ b/packages/mongodb-runner/src/index.ts @@ -7,6 +7,7 @@ export { MongoCluster, type MongoClusterEvents, MongoClusterOptions, + MongoDBUserDoc, } from './mongocluster'; export type { LogEntry } from './mongologreader'; export type { ConnectionString } from 'mongodb-connection-string-url'; diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index 871fe277..f215e8ac 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -3,23 +3,23 @@ import { MongoServer } from './mongoserver'; import { ConnectionString } from 'mongodb-connection-string-url'; import type { DownloadOptions } from '@mongodb-js/mongodb-downloader'; import { downloadMongoDb } from '@mongodb-js/mongodb-downloader'; -import type { MongoClientOptions } from 'mongodb'; +import type { Document, MongoClientOptions, TagSet } from 'mongodb'; import { MongoClient } from 'mongodb'; -import { sleep, range, uuid, debug } from './util'; +import { sleep, range, uuid, debug, jsonClone } from './util'; import { OIDCMockProviderProcess } from './oidc'; import { EventEmitter } from 'events'; +export interface MongoDBUserDoc { + username: string; + password: string; + customData?: Document; + roles: ({ role: string; db?: string } | string)[]; +} + export interface MongoClusterOptions extends Pick< MongoServerOptions, - | 'logDir' - | 'tmpDir' - | 'args' - | 'binDir' - | 'docker' - | 'login' - | 'password' - | 'clientOptions' + 'logDir' | 'tmpDir' | 'args' | 'binDir' | 'docker' > { topology: 'standalone' | 'replset' | 'sharded'; arbiters?: number; @@ -29,11 +29,11 @@ export interface MongoClusterOptions downloadDir?: string; downloadOptions?: DownloadOptions; oidc?: string; - rsTags?: { [key: string]: string }[]; + rsTags?: TagSet[]; rsArgs?: string[][]; shardArgs?: string[][]; mongosArgs?: string[][]; - roles?: { [key: string]: string }[]; + users?: MongoDBUserDoc[]; } export type MongoClusterEvents = { @@ -49,6 +49,8 @@ export class MongoCluster extends EventEmitter { private servers: MongoServer[] = []; // mongod/mongos private shards: MongoCluster[] = []; // replsets private oidcMockProviderProcess?: OIDCMockProviderProcess; + private defaultConnectionOptions: Partial = {}; + private users: MongoDBUserDoc[] = []; private constructor() { super(); @@ -87,17 +89,23 @@ export class MongoCluster extends EventEmitter { servers: this.servers.map((srv) => srv.serialize()), shards: this.shards.map((shard) => shard.serialize()), oidcMockProviderProcess: this.oidcMockProviderProcess?.serialize(), + defaultConnectionOptions: jsonClone(this.defaultConnectionOptions ?? {}), + users: jsonClone(this.users), }; } isClosed(): boolean { - return this.servers.length === 0 && this.shards.length === 0; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const _ of this.children()) return true; + return true; } static async deserialize(serialized: any): Promise { const cluster = new MongoCluster(); cluster.topology = serialized.topology; cluster.replSetName = serialized.replSetName; + cluster.defaultConnectionOptions = serialized.defaultConnectionOptions; + cluster.users = serialized.users; cluster.servers = await Promise.all( serialized.servers.map((srv: any) => MongoServer.deserialize(srv)), ); @@ -145,6 +153,7 @@ export class MongoCluster extends EventEmitter { }: MongoClusterOptions): Promise { const cluster = new MongoCluster(); cluster.topology = options.topology; + cluster.users = options.users ?? []; if (!options.binDir) { options.binDir = await this.downloadMongoDb( options.downloadDir ?? options.tmpDir, @@ -185,10 +194,6 @@ export class MongoCluster extends EventEmitter { binary: 'mongod', }), ); - if (options.login) { - await cluster.servers[0].addAdminUser(options.roles); - await cluster.servers[0].reinitialize(); - } } else if (options.topology === 'replset') { const { secondaries = 2, arbiters = 0 } = options; @@ -279,16 +284,6 @@ export class MongoCluster extends EventEmitter { debug('rs.status did not include primary, waiting...'); await sleep(1000); } - - // Add auth if needed - if (options.login) { - // Sleep to give time for the election to settle. - await sleep(1000); - await cluster.servers[0].addAdminUser(options.roles); - for (const server of cluster.servers) { - await server.reinitialize(); - } - } }); } else if (options.topology === 'sharded') { const { shards = 3 } = options; @@ -336,7 +331,6 @@ export class MongoCluster extends EventEmitter { ], }); cluster.servers.push(mongos); - await mongos.reinitialize(); await mongos.withClient(async (client) => { for (const shard of shardsvrs) { const shardSpec = `${shard.replSetName!}/${shard.hostport}`; @@ -349,13 +343,50 @@ export class MongoCluster extends EventEmitter { }); } } + + await cluster.addAuthIfNeeded(); return cluster; } + *children(): Iterable { + yield* this.servers; + yield* this.shards; + } + + async addAuthIfNeeded(): Promise { + if (!this.users?.length) return; + // Sleep to give time for a possible replset election to settle. + await sleep(1000); + await this.withClient(async (client) => { + const admin = client.db('admin'); + for (const user of this.users) { + const { username, password, ...rest } = user; + await admin.command({ createUser: username, pwd: password, ...rest }); + } + }); + await this.updateDefaultConnectionOptions({ + auth: this.users[0], + }); + } + + async updateDefaultConnectionOptions( + options: Partial, + ): Promise { + await Promise.all( + [...this.children()].map(async (child) => + child.updateDefaultConnectionOptions(options), + ), + ); + this.defaultConnectionOptions = { + ...this.defaultConnectionOptions, + ...options, + }; + } + async close(): Promise { await Promise.all( - [...this.servers, ...this.shards, this.oidcMockProviderProcess].map( - (closable) => closable?.close(), + [...this.children(), this.oidcMockProviderProcess].map((closable) => + closable?.close(), ), ); this.servers = []; @@ -366,10 +397,10 @@ export class MongoCluster extends EventEmitter { fn: Fn, clientOptions: MongoClientOptions = {}, ): Promise> { - const client = await MongoClient.connect( - this.connectionString, - clientOptions, - ); + const client = await MongoClient.connect(this.connectionString, { + ...this.defaultConnectionOptions, + ...clientOptions, + }); try { return await fn(client); } finally { @@ -378,10 +409,10 @@ export class MongoCluster extends EventEmitter { } ref(): void { - for (const child of [...this.servers, ...this.shards]) child.ref(); + for (const child of this.children()) child.ref(); } unref(): void { - for (const child of [...this.servers, ...this.shards]) child.unref(); + for (const child of this.children()) child.unref(); } } diff --git a/packages/mongodb-runner/src/mongoserver.ts b/packages/mongodb-runner/src/mongoserver.ts index c42f9d5d..bffaa748 100644 --- a/packages/mongodb-runner/src/mongoserver.ts +++ b/packages/mongodb-runner/src/mongoserver.ts @@ -13,7 +13,7 @@ import type { Document, MongoClientOptions } from 'mongodb'; import { MongoClient } from 'mongodb'; import path from 'path'; import { EventEmitter, once } from 'events'; -import { uuid, debug, pick, debugVerbose } from './util'; +import { uuid, debug, pick, debugVerbose, jsonClone } from './util'; export interface MongoServerOptions { binDir?: string; @@ -22,9 +22,6 @@ export interface MongoServerOptions { logDir?: string; // If set, pipe log file output through here. args?: string[]; // May or may not contain --port docker?: string | string[]; // Image or docker options - login?: string; // Simple user login. - password?: string; // Simple user password. - clientOptions?: { [key: string]: string }; // Extra options to pass to clients. } interface SerializedServerProperties { @@ -32,9 +29,9 @@ interface SerializedServerProperties { pid?: number; port?: number; dbPath?: string; + defaultConnectionOptions?: Partial; startTime: string; hasInsertedMetadataCollEntry: boolean; - hasAuth: boolean; } export interface MongoServerEvents { @@ -51,10 +48,7 @@ export class MongoServer extends EventEmitter { private closing = false; private startTime = new Date().toISOString(); private hasInsertedMetadataCollEntry = false; - private login?: string; - private password?: string; - private addAuth?: boolean; - private clientOptions?: { [key: string]: string }; + private defaultConnectionOptions?: Partial; get id(): string { return this.uuid; @@ -73,7 +67,7 @@ export class MongoServer extends EventEmitter { dbPath: this.dbPath, startTime: this.startTime, hasInsertedMetadataCollEntry: this.hasInsertedMetadataCollEntry, - hasAuth: this.login ? this.login.length > 0 : false, + defaultConnectionOptions: jsonClone(this.defaultConnectionOptions ?? {}), }; } @@ -83,11 +77,8 @@ export class MongoServer extends EventEmitter { const srv = new MongoServer(); srv.uuid = serialized._id; srv.port = serialized.port; - if (!serialized.hasAuth) { - srv.closing = !!(await srv._populateBuildInfo('restore-check')); - } else { - srv.closing = false; - } + srv.defaultConnectionOptions = serialized.defaultConnectionOptions; + srv.closing = !!(await srv._populateBuildInfo('restore-check')); if (!srv.closing) { srv.pid = serialized.pid; srv.dbPath = serialized.dbPath; @@ -156,8 +147,6 @@ export class MongoServer extends EventEmitter { ...options }: MongoServerOptions): Promise { const srv = new MongoServer(); - srv.clientOptions = options.clientOptions || {}; - if (!options.docker) { const dbPath = path.join(options.tmpDir, `db-${srv.uuid}`); await fs.mkdir(dbPath, { recursive: true }); @@ -278,14 +267,9 @@ export class MongoServer extends EventEmitter { logEntryStream.resume(); srv.port = port; - if (options.login === undefined) { - const buildInfoError = await srv._populateBuildInfo('insert-new'); - if (buildInfoError) { - debug('failed to get buildInfo', buildInfoError); - } - } else { - srv.login = options.login; - srv.password = options.password; + const buildInfoError = await srv._populateBuildInfo('insert-new'); + if (buildInfoError) { + debug('failed to get buildInfo', buildInfoError); } } catch (err) { await srv.close(); @@ -295,24 +279,24 @@ export class MongoServer extends EventEmitter { return srv; } - async addAdminUser(roles?: { [key: string]: string }[]) { - debug('adding admin user', this.hostport); - await this.withClient(async (client) => { - await client - .db('admin') - .command({ createUser: this.login, pwd: this.password, roles }); + async updateDefaultConnectionOptions( + options: Partial, + ): Promise { + const buildInfoError = await this._populateBuildInfo('restore-check', { + ...options, }); - } - - async reinitialize() { - debug('reinitializing', this.hostport); - if (this.login) { - this.addAuth = true; - } - const buildInfoError = await this._populateBuildInfo('insert-new'); if (buildInfoError) { - debug('failed to get buildInfo', buildInfoError); + debug( + 'failed to get buildInfo when setting new options', + buildInfoError, + options, + ); + throw buildInfoError; } + this.defaultConnectionOptions = { + ...this.defaultConnectionOptions, + ...options, + }; } async close(): Promise { @@ -355,7 +339,6 @@ export class MongoServer extends EventEmitter { 'port', 'dbPath', 'startTime', - 'hasAuth', ]); const runnerColl = client .db(isMongoS ? 'config' : 'local') @@ -391,10 +374,11 @@ export class MongoServer extends EventEmitter { private async _populateBuildInfo( mode: 'insert-new' | 'restore-check', + clientOpts?: Partial, ): Promise { try { // directConnection + retryWrites let us write to `local` db on secondaries - const clientOpts = { retryWrites: false }; + clientOpts = { retryWrites: false, ...clientOpts }; this.buildInfo = await this.withClient(async (client) => { // Insert the metadata entry, except if we're a freshly started mongos // (which does not have its own storage to persist) @@ -417,15 +401,9 @@ export class MongoServer extends EventEmitter { fn: Fn, clientOptions: MongoClientOptions = {}, ): Promise> { - let url: string; - if (this.addAuth) { - url = `mongodb://${this.login}:${this.password}@${this.hostport}/`; - } else { - url = `mongodb://${this.hostport}/`; - } - const client = await MongoClient.connect(url, { + const client = await MongoClient.connect(`mongodb://${this.hostport}/`, { directConnection: true, - ...this.clientOptions, + ...this.defaultConnectionOptions, ...clientOptions, }); try { diff --git a/packages/mongodb-runner/src/util.ts b/packages/mongodb-runner/src/util.ts index 57507397..27d748f8 100644 --- a/packages/mongodb-runner/src/util.ts +++ b/packages/mongodb-runner/src/util.ts @@ -42,3 +42,7 @@ export function pick( } return ret as Pick; } + +export function jsonClone(obj: T): T { + return JSON.parse(JSON.stringify(obj)); +} From 5e383ec713cce3d62986e911c944ec3f41dceef2 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 14 Nov 2025 05:45:31 -0600 Subject: [PATCH 06/22] fix handling of arbiters --- packages/mongodb-runner/src/mongocluster.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index f215e8ac..31eb29c0 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -256,7 +256,7 @@ export class MongoCluster extends EventEmitter { return { _id: i, host: srv.hostport, - arbiterOnly: i > 1 + secondaries, + arbiterOnly: i > secondaries, priority: i === 0 ? 1 : 0, tags, }; From e4756968e0069931282cbc3ceb1afb21a4e67f23 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 14 Nov 2025 09:53:24 -0600 Subject: [PATCH 07/22] Add RSMemberOptions --- packages/mongodb-runner/src/mongocluster.ts | 34 +++++++++++++-------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index 31eb29c0..8e01c0f2 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -16,6 +16,11 @@ export interface MongoDBUserDoc { roles: ({ role: string; db?: string } | string)[]; } +export interface RSMemberOptions { + tags?: { [key: string]: string }; + priority?: number; + args?: string[]; +} export interface MongoClusterOptions extends Pick< MongoServerOptions, @@ -29,8 +34,7 @@ export interface MongoClusterOptions downloadDir?: string; downloadOptions?: DownloadOptions; oidc?: string; - rsTags?: TagSet[]; - rsArgs?: string[][]; + rsMemberOptions?: RSMemberOptions[]; shardArgs?: string[][]; mongosArgs?: string[][]; users?: MongoDBUserDoc[]; @@ -207,9 +211,9 @@ export class MongoCluster extends EventEmitter { } const primaryArgs = [...args]; - const rsArgs = options.rsArgs || [[]]; - if (rsArgs.length > 0) { - primaryArgs.push(...rsArgs[0]); + const rsMemberOptions = options.rsMemberOptions || [{}]; + if (rsMemberOptions.length > 0) { + primaryArgs.push(...(rsMemberOptions[0].args || [])); } debug('Starting primary', primaryArgs); const primary = await MongoServer.start({ @@ -232,9 +236,9 @@ export class MongoCluster extends EventEmitter { ...(await Promise.all( range(secondaries + arbiters).map((i) => { const secondaryArgs = [...args]; - if (i + 1 < rsArgs.length) { - secondaryArgs.push(...rsArgs[i + 1]); - debug('Adding secondary args', rsArgs[i + 1]); + if (i + 1 < rsMemberOptions.length) { + secondaryArgs.push(...(rsMemberOptions[i + 1].args || [])); + debug('Adding secondary args', rsMemberOptions[i + 1].args || []); } return MongoServer.start({ ...options, @@ -247,18 +251,24 @@ export class MongoCluster extends EventEmitter { await primary.withClient(async (client) => { debug('Running rs.initiate'); - const rsTags = options.rsTags || [{}]; const rsConf = { _id: replSetName, configsvr: args.includes('--configsvr'), members: cluster.servers.map((srv, i) => { - const tags = i < rsTags.length || 0 ? rsTags[i] : {}; + let options: RSMemberOptions = {}; + if (i < rsMemberOptions.length) { + options = rsMemberOptions[i]; + } + let priority = i === 0 ? 1 : 0; + if (options.priority !== undefined) { + priority = options.priority; + } return { _id: i, host: srv.hostport, arbiterOnly: i > secondaries, - priority: i === 0 ? 1 : 0, - tags, + priority, + tags: options.tags || {}, }; }), }; From 625c540107291e5d4f7568a8a52bce043190ad78 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 17 Nov 2025 14:17:53 +0100 Subject: [PATCH 08/22] fixup: use actual TagSet type --- packages/mongodb-runner/src/mongocluster.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index 8e01c0f2..d7249a85 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -17,7 +17,7 @@ export interface MongoDBUserDoc { } export interface RSMemberOptions { - tags?: { [key: string]: string }; + tags?: TagSet; priority?: number; args?: string[]; } From 47de6fd293075a7f2d05e41d8c9cc1d66cc59a49 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 17 Nov 2025 14:28:49 +0100 Subject: [PATCH 09/22] feat(mongodb-runner): add ability to use a config file --- packages/mongodb-runner/src/cli.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mongodb-runner/src/cli.ts b/packages/mongodb-runner/src/cli.ts index a63472dd..375f0e76 100644 --- a/packages/mongodb-runner/src/cli.ts +++ b/packages/mongodb-runner/src/cli.ts @@ -74,6 +74,7 @@ import type { MongoClientOptions } from 'mongodb'; type: 'string', describe: 'Configure OIDC authentication on the server', }) + .config() .option('debug', { type: 'boolean', describe: 'Enable debug output' }) .command('start', 'Start a MongoDB instance') .command('stop', 'Stop a MongoDB instance') From 4c756a11f5d30585bfe4bd6120deff1781507c54 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 18 Nov 2025 01:58:13 +0100 Subject: [PATCH 10/22] fixup: clean up replset and shard arg handling --- packages/mongodb-runner/src/mongocluster.ts | 261 +++++++++++++------- 1 file changed, 171 insertions(+), 90 deletions(-) diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index d7249a85..64c40411 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -8,6 +8,7 @@ import { MongoClient } from 'mongodb'; import { sleep, range, uuid, debug, jsonClone } from './util'; import { OIDCMockProviderProcess } from './oidc'; import { EventEmitter } from 'events'; +import assert from 'assert'; export interface MongoDBUserDoc { username: string; @@ -20,26 +21,54 @@ export interface RSMemberOptions { tags?: TagSet; priority?: number; args?: string[]; + arbiterOnly?: boolean; } -export interface MongoClusterOptions - extends Pick< - MongoServerOptions, - 'logDir' | 'tmpDir' | 'args' | 'binDir' | 'docker' - > { - topology: 'standalone' | 'replset' | 'sharded'; - arbiters?: number; - secondaries?: number; - shards?: number; - version?: string; + +export interface CommonOptions { downloadDir?: string; downloadOptions?: DownloadOptions; + oidc?: string; - rsMemberOptions?: RSMemberOptions[]; - shardArgs?: string[][]; - mongosArgs?: string[][]; + + version?: string; users?: MongoDBUserDoc[]; + + topology: 'standalone' | 'replset' | 'sharded'; } +export type RSOptions = + | { + arbiters?: number; + secondaries?: number; + rsMembers?: never; + } + | { + arbiters?: never; + secondaries?: never; + rsMembers: RSMemberOptions[]; + }; + +export type ShardedOptions = { + mongosArgs?: string[][]; +} & ( + | { + shards?: number; + shardArgs?: never; + } + | { + shards?: never; + shardArgs?: string[][]; + } +); + +export type MongoClusterOptions = Pick< + MongoServerOptions, + 'logDir' | 'tmpDir' | 'args' | 'binDir' | 'docker' +> & + CommonOptions & + RSOptions & + ShardedOptions; + export type MongoClusterEvents = { [k in keyof MongoServerEvents]: [serverUUID: string, ...MongoServerEvents[k]]; } & { @@ -47,6 +76,108 @@ export type MongoClusterEvents = { removeListener: [keyof MongoClusterEvents]; }; +function removePortArg([...args]: string[]): string[] { + let portArgIndex = -1; + if ((portArgIndex = args.indexOf('--port')) !== -1) { + args.splice(portArgIndex + 1, 1); + } else if ( + (portArgIndex = args.findIndex((arg) => arg.startsWith('--port='))) !== -1 + ) { + args.splice(portArgIndex, 1); + } + return args; +} + +function hasPortArg(args: string[] | undefined): boolean { + if (!args) return false; + return ( + args.includes('--port') || args.some((arg) => arg.startsWith('--port=')) + ); +} + +function processRSMembers(options: MongoClusterOptions): { + rsMembers: RSMemberOptions[]; + replSetName: string; +} { + const { + secondaries = 2, + arbiters = 0, + args: [...args] = [], + rsMembers, + } = options; + + let replSetName: string; + if (!args.includes('--replSet')) { + replSetName = `replSet-${uuid()}`; + args.push('--replSet', replSetName); + } else { + replSetName = args[args.indexOf('--replSet') + 1]; + } + + const primaryArgs: string[] = [...args]; + const secondaryArgs = [...removePortArg(args), '--port', '0']; + + if (rsMembers) { + const primary = rsMembers.find((m) => + rsMembers.every((m2) => m.priority ?? 0 >= (m2.priority ?? 0)), + ); + return { + rsMembers: rsMembers.map((m) => ({ + ...m, + args: [ + ...(m.args ?? []), + ...(hasPortArg(m.args) + ? args + : m === primary + ? primaryArgs + : secondaryArgs), + ], + })), + replSetName, + }; + } + + return { + rsMembers: [ + { priority: 1, args: primaryArgs }, + ...range(secondaries).map(() => ({ priority: 0, args: secondaryArgs })), + ...range(arbiters).map(() => ({ + priority: 0, + arbiterOnly: true, + args: secondaryArgs, + })), + ], + replSetName, + }; +} + +function processShardOptions(options: MongoClusterOptions): { + shardArgs: string[][]; + mongosArgs: string[][]; +} { + const { + shardArgs = range(options.shards ?? 1).map(() => []), + mongosArgs = [[]], + args = [], + } = options; + return { + shardArgs: shardArgs.map((perShardArgs, i) => [ + ...removePortArg(args), + ...perShardArgs, + ...(perShardArgs.includes('--configsvr') || + perShardArgs.includes('--shardsvr') + ? [] + : i === 0 + ? ['--configsvr'] + : ['--shardsvr']), + ]), + mongosArgs: mongosArgs.map((perMongosArgs, i) => [ + ...(i === 0 && !hasPortArg(perMongosArgs) ? args : removePortArg(args)), + ...perMongosArgs, + ]), + }; +} + export class MongoCluster extends EventEmitter { private topology: MongoClusterOptions['topology'] = 'standalone'; private replSetName?: string; @@ -199,76 +330,45 @@ export class MongoCluster extends EventEmitter { }), ); } else if (options.topology === 'replset') { - const { secondaries = 2, arbiters = 0 } = options; - - const args = [...(options.args ?? [])]; - let replSetName: string; - if (!args.includes('--replSet')) { - replSetName = `replSet-${uuid()}`; - args.push('--replSet', replSetName); - } else { - replSetName = args[args.indexOf('--replSet') + 1]; - } + const { rsMembers, replSetName } = processRSMembers(options); - const primaryArgs = [...args]; - const rsMemberOptions = options.rsMemberOptions || [{}]; - if (rsMemberOptions.length > 0) { - primaryArgs.push(...(rsMemberOptions[0].args || [])); - } - debug('Starting primary', primaryArgs); - const primary = await MongoServer.start({ - ...options, - args: primaryArgs, - binary: 'mongod', + debug('Starting replica set nodes', { + replSetName, + secondaries: rsMembers.filter((m) => !m.arbiterOnly).length - 1, + arbiters: rsMembers.filter((m) => m.arbiterOnly).length, }); - cluster.servers.push(primary); - - if (args.includes('--port')) { - args.splice(args.indexOf('--port') + 1, 1, '0'); - } + const primaryIndex = rsMembers.findIndex((m) => + rsMembers.every((m2) => m.priority ?? 0 >= (m2.priority ?? 0)), + ); + assert.notStrictEqual(primaryIndex, -1); - debug('Starting secondaries and arbiters', { - secondaries, - arbiters, - args, - }); - cluster.servers.push( - ...(await Promise.all( - range(secondaries + arbiters).map((i) => { - const secondaryArgs = [...args]; - if (i + 1 < rsMemberOptions.length) { - secondaryArgs.push(...(rsMemberOptions[i + 1].args || [])); - debug('Adding secondary args', rsMemberOptions[i + 1].args || []); - } - return MongoServer.start({ + const nodes = await Promise.all( + rsMembers.map(async (member) => { + return [ + await MongoServer.start({ ...options, - args: secondaryArgs, + args: member.args, binary: 'mongod', - }); - }), - )), + }), + member, + ] as const; + }), ); + cluster.servers.push(...nodes.map(([srv]) => srv)); + const primary = cluster.servers[primaryIndex]; await primary.withClient(async (client) => { debug('Running rs.initiate'); const rsConf = { _id: replSetName, - configsvr: args.includes('--configsvr'), - members: cluster.servers.map((srv, i) => { - let options: RSMemberOptions = {}; - if (i < rsMemberOptions.length) { - options = rsMemberOptions[i]; - } - let priority = i === 0 ? 1 : 0; - if (options.priority !== undefined) { - priority = options.priority; - } + configsvr: rsMembers.some((m) => m.args?.includes('--configsvr')), + members: nodes.map(([srv, member], i) => { return { _id: i, host: srv.hostport, - arbiterOnly: i > secondaries, - priority, - tags: options.tags || {}, + arbiterOnly: member.arbiterOnly ?? false, + priority: member.priority ?? 1, + tags: member.tags || {}, }; }), }; @@ -296,28 +396,10 @@ export class MongoCluster extends EventEmitter { } }); } else if (options.topology === 'sharded') { - const { shards = 3 } = options; - const shardArgs = [...(options.args ?? [])]; - if (shardArgs.includes('--port')) { - shardArgs.splice(shardArgs.indexOf('--port') + 1, 1, '0'); - } - const perShardArgs = options.shardArgs || [[]]; - + const { shardArgs, mongosArgs } = processShardOptions(options); debug('starting config server and shard servers', shardArgs); const [configsvr, ...shardsvrs] = await Promise.all( - range(shards + 1).map((i) => { - const args: string[] = [...shardArgs]; - if (i === 0) { - args.push('--configsvr'); - } else { - if (i - 1 < perShardArgs.length) { - args.push(...perShardArgs[i - 1]); - debug('Adding shard args', perShardArgs[i - 1]); - } - if (!args.includes('--shardsvr')) { - args.push('--shardsvr'); - } - } + shardArgs.map((args) => { return MongoCluster.start({ ...options, args, @@ -327,7 +409,6 @@ export class MongoCluster extends EventEmitter { ); cluster.shards.push(configsvr, ...shardsvrs); - const mongosArgs = options.mongosArgs ?? [[]]; for (let i = 0; i < mongosArgs.length; i++) { debug('starting mongos'); const mongos = await MongoServer.start({ From 23bb0d3784416a7aa39705a7961f27ec98e6ea3b Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 18 Nov 2025 02:04:01 +0100 Subject: [PATCH 11/22] fixup: allow passing CLI args as env vars, add verbose debug mode directly --- packages/mongodb-runner/src/cli.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/mongodb-runner/src/cli.ts b/packages/mongodb-runner/src/cli.ts index 375f0e76..f497a60d 100644 --- a/packages/mongodb-runner/src/cli.ts +++ b/packages/mongodb-runner/src/cli.ts @@ -75,7 +75,9 @@ import type { MongoClientOptions } from 'mongodb'; describe: 'Configure OIDC authentication on the server', }) .config() + .env('MONGODB_RUNNER') .option('debug', { type: 'boolean', describe: 'Enable debug output' }) + .option('verbose', { type: 'boolean', describe: 'Enable verbose output' }) .command('start', 'Start a MongoDB instance') .command('stop', 'Stop a MongoDB instance') .command('prune', 'Clean up metadata for any dead MongoDB instances') @@ -87,9 +89,12 @@ import type { MongoClientOptions } from 'mongodb'; .demandCommand(1, 'A command needs to be provided') .help().argv; const [command, ...args] = argv._.map(String); - if (argv.debug) { + if (argv.debug || argv.verbose) { createDebug.enable('mongodb-runner'); } + if (argv.verbose) { + createDebug.enable('mongodb-runner:*'); + } if (argv.oidc && process.platform !== 'linux') { console.warn( From d159b3d473fd13fffeb67cdfa0399704e869bcf1 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 18 Nov 2025 02:04:16 +0100 Subject: [PATCH 12/22] fixup: be more correct/more explicit in replset hosts test --- packages/mongodb-runner/src/mongocluster.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/mongodb-runner/src/mongocluster.spec.ts b/packages/mongodb-runner/src/mongocluster.spec.ts index 9f1717bc..3e4ae273 100644 --- a/packages/mongodb-runner/src/mongocluster.spec.ts +++ b/packages/mongodb-runner/src/mongocluster.spec.ts @@ -125,7 +125,9 @@ describe('MongoCluster', function () { const hello = await cluster.withClient(async (client) => { return await client.db('admin').command({ hello: 1 }); }); - expect(+hello.passives.length + +hello.hosts.length).to.equal(5); + expect(hello.hosts).to.have.lengthOf(1); + expect(hello.passives).to.have.lengthOf(3); + expect(hello.arbiters).to.have.lengthOf(1); }); it('can spawn a 6.x sharded cluster', async function () { From fea6cebe2629c8b47e023f1f164d4f48fec03da8 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 18 Nov 2025 03:45:54 +0100 Subject: [PATCH 13/22] add tests, fix behaviors --- .../mongodb-runner/src/mongocluster.spec.ts | 168 ++++++++++++++++++ packages/mongodb-runner/src/mongocluster.ts | 93 ++++++---- packages/mongodb-runner/src/mongoserver.ts | 32 +++- packages/mongodb-runner/src/util.ts | 20 +++ 4 files changed, 272 insertions(+), 41 deletions(-) diff --git a/packages/mongodb-runner/src/mongocluster.spec.ts b/packages/mongodb-runner/src/mongocluster.spec.ts index 3e4ae273..bb776d21 100644 --- a/packages/mongodb-runner/src/mongocluster.spec.ts +++ b/packages/mongodb-runner/src/mongocluster.spec.ts @@ -345,4 +345,172 @@ describe('MongoCluster', function () { baddoc: 1, }); }); + + it('can pass custom arguments for replica set members', async function () { + cluster = await MongoCluster.start({ + version: '6.x', + topology: 'replset', + tmpDir, + rsMembers: [ + { args: ['--setParameter', 'cursorTimeoutMillis=60000'] }, + { args: ['--setParameter', 'cursorTimeoutMillis=50000'] }, + ], + }); + + expect(cluster.connectionString).to.be.a('string'); + expect(cluster.serverVersion).to.match(/^6\./); + const hello = await cluster.withClient(async (client) => { + return await client.db('admin').command({ hello: 1 }); + }); + expect(hello.hosts).to.have.lengthOf(1); + expect(hello.passives).to.have.lengthOf(1); + + const servers = cluster['servers']; + expect(servers).to.have.lengthOf(2); + const values = await Promise.all( + servers.map((srv) => + srv.withClient(async (client) => { + return await Promise.all([ + client + .db('admin') + .command({ getParameter: 1, cursorTimeoutMillis: 1 }), + client.db('admin').command({ hello: 1 }), + ]); + }), + ), + ); + + expect( + values.map((v) => [v[0].cursorTimeoutMillis, v[1].isWritablePrimary]), + ).to.deep.equal([ + [60000, true], + [50000, false], + ]); + }); + + it('can pass custom arguments for shards', async function () { + cluster = await MongoCluster.start({ + version: '6.x', + topology: 'sharded', + tmpDir, + secondaries: 0, + shardArgs: [ + ['--setParameter', 'cursorTimeoutMillis=60000'], + ['--setParameter', 'cursorTimeoutMillis=50000'], + ], + }); + + expect(cluster.connectionString).to.be.a('string'); + expect(cluster.serverVersion).to.match(/^6\./); + + const shards = cluster['shards']; + expect(shards).to.have.lengthOf(2); + const values = await Promise.all( + shards.map((srv) => + srv.withClient(async (client) => { + return await Promise.all([ + client + .db('admin') + .command({ getParameter: 1, cursorTimeoutMillis: 1 }), + client.db('admin').command({ hello: 1 }), + ]); + }), + ), + ); + + expect( + values.map((v) => [ + v[0].cursorTimeoutMillis, + v[1].setName === values[0][1].setName, + ]), + ).to.deep.equal([ + [60000, true], + [50000, false], + ]); + }); + + it('can pass custom arguments for mongoses', async function () { + cluster = await MongoCluster.start({ + version: '6.x', + topology: 'sharded', + tmpDir, + secondaries: 0, + mongosArgs: [ + ['--setParameter', 'cursorTimeoutMillis=60000'], + ['--setParameter', 'cursorTimeoutMillis=50000'], + ], + }); + + expect(cluster.connectionString).to.be.a('string'); + expect(cluster.serverVersion).to.match(/^6\./); + + const mongoses = cluster['servers']; + expect(mongoses).to.have.lengthOf(2); + const values = await Promise.all( + mongoses.map((srv) => + srv.withClient(async (client) => { + return await Promise.all([ + client + .db('admin') + .command({ getParameter: 1, cursorTimeoutMillis: 1 }), + client.db('admin').command({ hello: 1 }), + ]); + }), + ), + ); + + const processIdForMongos = (v: any) => + v[1].topologyVersion.processId.toHexString(); + expect( + values.map((v) => [ + v[0].cursorTimeoutMillis, + v[1].msg, + processIdForMongos(v) === processIdForMongos(values[0]), + ]), + ).to.deep.equal([ + [60000, 'isdbgrid', true], + [50000, 'isdbgrid', false], + ]); + }); + + it('can add authentication options and verify them after serialization', async function () { + cluster = await MongoCluster.start({ + version: '6.x', + topology: 'sharded', + tmpDir, + secondaries: 1, + shards: 1, + users: [ + { + username: 'testuser', + password: 'testpass', + roles: [{ role: 'readWriteAnyDatabase', db: 'admin' }], + }, + ], + mongosArgs: [[], []], + }); + expect(cluster.connectionString).to.be.a('string'); + expect(cluster.serverVersion).to.match(/^6\./); + expect(cluster.connectionString).to.include('testuser:testpass@'); + await cluster.withClient(async (client) => { + const result = await client + .db('test') + .collection('test') + .insertOne({ foo: 42 }); + expect(result.insertedId).to.exist; + }); + + cluster = await MongoCluster.deserialize(cluster.serialize()); + expect(cluster.connectionString).to.include('testuser:testpass@'); + const [doc, status] = await cluster.withClient(async (client) => { + return Promise.all([ + client.db('test').collection('test').findOne({ foo: 42 }), + client.db('admin').command({ connectionStatus: 1 }), + ] as const); + }); + expect(doc?.foo).to.equal(42); + expect(status.authInfo.authenticatedUsers).to.deep.equal([ + { user: 'testuser', db: 'admin' }, + ]); + }); }); diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index 64c40411..1b390ba1 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -5,7 +5,15 @@ import type { DownloadOptions } from '@mongodb-js/mongodb-downloader'; import { downloadMongoDb } from '@mongodb-js/mongodb-downloader'; import type { Document, MongoClientOptions, TagSet } from 'mongodb'; import { MongoClient } from 'mongodb'; -import { sleep, range, uuid, debug, jsonClone } from './util'; +import { + sleep, + range, + uuid, + debug, + jsonClone, + debugVerbose, + makeConnectionString, +} from './util'; import { OIDCMockProviderProcess } from './oidc'; import { EventEmitter } from 'events'; import assert from 'assert'; @@ -156,7 +164,7 @@ function processShardOptions(options: MongoClusterOptions): { mongosArgs: string[][]; } { const { - shardArgs = range(options.shards ?? 1).map(() => []), + shardArgs = range((options.shards ?? 3) + 1).map(() => []), mongosArgs = [[]], args = [], } = options; @@ -258,13 +266,11 @@ export class MongoCluster extends EventEmitter { } get connectionString(): string { - const cs = new ConnectionString(`mongodb://${this.hostport}/`); - if (this.replSetName) - cs.typedSearchParams().set( - 'replicaSet', - this.replSetName, - ); - return cs.toString(); + return makeConnectionString( + this.hostport, + this.replSetName, + this.defaultConnectionOptions, + ); } get oidcIssuer(): string | undefined { @@ -367,11 +373,12 @@ export class MongoCluster extends EventEmitter { _id: i, host: srv.hostport, arbiterOnly: member.arbiterOnly ?? false, - priority: member.priority ?? 1, + priority: member.priority ?? (i === primaryIndex ? 1 : 0), tags: member.tags || {}, }; }), }; + debugVerbose('replSetInitiate:', rsConf); await client.db('admin').command({ replSetInitiate: rsConf, }); @@ -398,48 +405,58 @@ export class MongoCluster extends EventEmitter { } else if (options.topology === 'sharded') { const { shardArgs, mongosArgs } = processShardOptions(options); debug('starting config server and shard servers', shardArgs); - const [configsvr, ...shardsvrs] = await Promise.all( - shardArgs.map((args) => { - return MongoCluster.start({ + const allShards = await Promise.all( + shardArgs.map(async (args) => { + const isConfig = args.includes('--configsvr'); + const cluster = await MongoCluster.start({ ...options, args, topology: 'replset', + users: isConfig ? undefined : options.users, // users go on the mongos/config server only for the config set }); + return [cluster, isConfig] as const; }), ); + const configsvr = allShards.find(([, isConfig]) => isConfig)![0]; + const shardsvrs = allShards + .filter(([, isConfig]) => !isConfig) + .map(([shard]) => shard); cluster.shards.push(configsvr, ...shardsvrs); - for (let i = 0; i < mongosArgs.length; i++) { - debug('starting mongos'); - const mongos = await MongoServer.start({ - ...options, - binary: 'mongos', - args: [ - ...(options.args ?? []), - ...mongosArgs[i], - '--configdb', - `${configsvr.replSetName!}/${configsvr.hostport}`, - ], - }); - cluster.servers.push(mongos); - await mongos.withClient(async (client) => { - for (const shard of shardsvrs) { - const shardSpec = `${shard.replSetName!}/${shard.hostport}`; - debug('adding shard', shardSpec); - await client.db('admin').command({ - addShard: shardSpec, - }); - } - debug('added shards'); - }); - } + const mongosServers: MongoServer[] = await Promise.all( + mongosArgs.map(async (args) => { + debug('starting mongos'); + return await MongoServer.start({ + ...options, + binary: 'mongos', + args: [ + ...(options.args ?? []), + ...args, + '--configdb', + `${configsvr.replSetName!}/${configsvr.hostport}`, + ], + }); + }), + ); + cluster.servers.push(...mongosServers); + const mongos = mongosServers[0]; + await mongos.withClient(async (client) => { + for (const shard of shardsvrs) { + const shardSpec = `${shard.replSetName!}/${shard.hostport}`; + debug('adding shard', shardSpec); + await client.db('admin').command({ + addShard: shardSpec, + }); + } + debug('added shards'); + }); } await cluster.addAuthIfNeeded(); return cluster; } - *children(): Iterable { + private *children(): Iterable { yield* this.servers; yield* this.shards; } diff --git a/packages/mongodb-runner/src/mongoserver.ts b/packages/mongodb-runner/src/mongoserver.ts index bffaa748..cb4082cc 100644 --- a/packages/mongodb-runner/src/mongoserver.ts +++ b/packages/mongodb-runner/src/mongoserver.ts @@ -13,7 +13,14 @@ import type { Document, MongoClientOptions } from 'mongodb'; import { MongoClient } from 'mongodb'; import path from 'path'; import { EventEmitter, once } from 'events'; -import { uuid, debug, pick, debugVerbose, jsonClone } from './util'; +import { + uuid, + debug, + pick, + debugVerbose, + jsonClone, + makeConnectionString, +} from './util'; export interface MongoServerOptions { binDir?: string; @@ -345,8 +352,17 @@ export class MongoServer extends EventEmitter { .collection< Omit >('mongodbrunner'); + // mongos hosts require a bit of special treatment because they do not have + // local storage of their own, so we store the metadata in the config database, + // which may be accessed by multiple mongos instances. debug('ensuring metadata collection entry', insertedInfo, { isMongoS }); if (mode === 'insert-new') { + const existingEntry = await runnerColl.findOne(); + if (!isMongoS && existingEntry) { + throw new Error( + `Unexpected mongodbrunner entry when creating new server: ${JSON.stringify(existingEntry)}`, + ); + } await runnerColl.insertOne(insertedInfo); debug('inserted metadata collection entry', insertedInfo); this.hasInsertedMetadataCollEntry = true; @@ -357,7 +373,9 @@ export class MongoServer extends EventEmitter { ); return; } - const match = await runnerColl.findOne(); + const match = await runnerColl.findOne( + isMongoS ? { _id: this.uuid } : {}, + ); debug('read metadata collection entry', insertedInfo, match); if (!match) { throw new Error( @@ -397,11 +415,19 @@ export class MongoServer extends EventEmitter { return null; } + get connectionString(): string { + return makeConnectionString( + this.hostport, + undefined, + this.defaultConnectionOptions, + ); + } + async withClient any>( fn: Fn, clientOptions: MongoClientOptions = {}, ): Promise> { - const client = await MongoClient.connect(`mongodb://${this.hostport}/`, { + const client = await MongoClient.connect(this.connectionString, { directConnection: true, ...this.defaultConnectionOptions, ...clientOptions, diff --git a/packages/mongodb-runner/src/util.ts b/packages/mongodb-runner/src/util.ts index 27d748f8..4b49df77 100644 --- a/packages/mongodb-runner/src/util.ts +++ b/packages/mongodb-runner/src/util.ts @@ -1,5 +1,7 @@ +import type { MongoClientOptions } from 'mongodb'; import { BSON } from 'mongodb'; import createDebug from 'debug'; +import { ConnectionString } from 'mongodb-connection-string-url'; export const debug = createDebug('mongodb-runner'); export const debugVerbose = debug.extend('verbose'); @@ -46,3 +48,21 @@ export function pick( export function jsonClone(obj: T): T { return JSON.parse(JSON.stringify(obj)); } + +export function makeConnectionString( + hostport: string, + replSetName?: string, + defaultConnectionOptions: Partial = {}, +): string { + const cs = new ConnectionString(`mongodb://${hostport}/`); + if (replSetName) { + cs.typedSearchParams().set('replicaSet', replSetName); + } + if (defaultConnectionOptions.auth?.username) { + cs.username = defaultConnectionOptions.auth.username; + } + if (defaultConnectionOptions.auth?.password) { + cs.password = defaultConnectionOptions.auth.password; + } + return cs.toString(); +} From 74b8c933ff4e2cf51cd4cfb5fa6378591803ae9c Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 18 Nov 2025 03:54:50 +0100 Subject: [PATCH 14/22] fixup: check mongos list in config db --- packages/mongodb-runner/src/mongocluster.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/mongodb-runner/src/mongocluster.spec.ts b/packages/mongodb-runner/src/mongocluster.spec.ts index bb776d21..a057a1d4 100644 --- a/packages/mongodb-runner/src/mongocluster.spec.ts +++ b/packages/mongodb-runner/src/mongocluster.spec.ts @@ -471,6 +471,12 @@ describe('MongoCluster', function () { [60000, 'isdbgrid', true], [50000, 'isdbgrid', false], ]); + + const mongosList = await cluster.withClient( + async (client) => + await client.db('config').collection('mongos').find().toArray(), + ); + expect(mongosList).to.have.lengthOf(2); }); it('can add authentication options and verify them after serialization', async function () { From 008bfdf8506e61fea97517f2c807e9abf9120ac4 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 18 Nov 2025 13:47:51 +0100 Subject: [PATCH 15/22] fixup: CR, use 8.x in tests by default --- .../mongodb-runner/src/mongocluster.spec.ts | 55 ++++++++++++------- packages/mongodb-runner/src/mongocluster.ts | 1 + 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/packages/mongodb-runner/src/mongocluster.spec.ts b/packages/mongodb-runner/src/mongocluster.spec.ts index a057a1d4..73fce638 100644 --- a/packages/mongodb-runner/src/mongocluster.spec.ts +++ b/packages/mongodb-runner/src/mongocluster.spec.ts @@ -50,7 +50,7 @@ describe('MongoCluster', function () { try { cluster = await MongoCluster.start({ - version: '6.x', + version: '8.x', topology: 'standalone', tmpDir, downloadDir: customDownloadDir, @@ -65,7 +65,7 @@ describe('MongoCluster', function () { expect(MongoCluster['downloadMongoDb']).to.have.been.calledWith( customDownloadDir, - '6.x', + '8.x', { platform: 'linux', arch: 'x64', @@ -88,9 +88,24 @@ describe('MongoCluster', function () { expect(ok).to.equal(1); }); - it('can spawn a 6.x standalone mongod on a pre-specified port', async function () { + it('can spawn a 8.x standalone mongod', async function () { cluster = await MongoCluster.start({ - version: '6.x', + version: '8.x', + topology: 'standalone', + tmpDir, + }); + expect(cluster.connectionString).to.be.a('string'); + expect(cluster.serverVersion).to.match(/^6\./); + expect(cluster.serverVariant).to.equal('community'); + const { ok } = await cluster.withClient(async (client) => { + return await client.db('admin').command({ ping: 1 }); + }); + expect(ok).to.equal(1); + }); + + it('can spawn a 8.x standalone mongod on a pre-specified port', async function () { + cluster = await MongoCluster.start({ + version: '8.x', topology: 'standalone', tmpDir, args: ['--port', '50079'], @@ -98,9 +113,9 @@ describe('MongoCluster', function () { expect(cluster.connectionString).to.include(`:50079`); }); - it('can spawn a 6.x replset', async function () { + it('can spawn a 8.x replset', async function () { cluster = await MongoCluster.start({ - version: '6.x', + version: '8.x', topology: 'replset', tmpDir, }); @@ -112,9 +127,9 @@ describe('MongoCluster', function () { expect(+hello.passives.length + +hello.hosts.length).to.equal(3); }); - it('can spawn a 6.x replset with specific number of arbiters and secondaries', async function () { + it('can spawn a 8.x replset with specific number of arbiters and secondaries', async function () { cluster = await MongoCluster.start({ - version: '6.x', + version: '8.x', topology: 'replset', tmpDir, secondaries: 3, @@ -130,10 +145,10 @@ describe('MongoCluster', function () { expect(hello.arbiters).to.have.lengthOf(1); }); - it('can spawn a 6.x sharded cluster', async function () { + it('can spawn a 8.x sharded cluster', async function () { const logDir = path.join(tmpDir, `sharded-logs`); cluster = await MongoCluster.start({ - version: '6.x', + version: '8.x', topology: 'sharded', tmpDir, shards: 2, @@ -156,9 +171,9 @@ describe('MongoCluster', function () { ).to.equal(1); }); - it('can spawn a 6.x standalone mongod with TLS enabled and get build info', async function () { + it('can spawn a 8.x standalone mongod with TLS enabled and get build info', async function () { cluster = await MongoCluster.start({ - version: '6.x', + version: '8.x', topology: 'standalone', tmpDir, args: [ @@ -289,7 +304,7 @@ describe('MongoCluster', function () { it('can serialize and deserialize sharded cluster info', async function () { cluster = await MongoCluster.start({ - version: '6.x', + version: '8.x', topology: 'sharded', tmpDir, secondaries: 0, @@ -304,7 +319,7 @@ describe('MongoCluster', function () { it('can let callers listen for server log events', async function () { cluster = await MongoCluster.start({ - version: '6.x', + version: '8.x', topology: 'replset', tmpDir, secondaries: 1, @@ -348,7 +363,7 @@ describe('MongoCluster', function () { it('can pass custom arguments for replica set members', async function () { cluster = await MongoCluster.start({ - version: '6.x', + version: '8.x', topology: 'replset', tmpDir, rsMembers: [ @@ -390,7 +405,7 @@ describe('MongoCluster', function () { it('can pass custom arguments for shards', async function () { cluster = await MongoCluster.start({ - version: '6.x', + version: '8.x', topology: 'sharded', tmpDir, secondaries: 0, @@ -431,7 +446,7 @@ describe('MongoCluster', function () { it('can pass custom arguments for mongoses', async function () { cluster = await MongoCluster.start({ - version: '6.x', + version: '8.x', topology: 'sharded', tmpDir, secondaries: 0, @@ -447,13 +462,15 @@ describe('MongoCluster', function () { const mongoses = cluster['servers']; expect(mongoses).to.have.lengthOf(2); const values = await Promise.all( - mongoses.map((srv) => + mongoses.map((srv, i) => srv.withClient(async (client) => { return await Promise.all([ client .db('admin') .command({ getParameter: 1, cursorTimeoutMillis: 1 }), client.db('admin').command({ hello: 1 }), + // Ensure that the mongos announces itself to the cluster + client.db('test').collection(`test${i}`).insertOne({ dummy: 1 }), ]); }), ), @@ -481,7 +498,7 @@ describe('MongoCluster', function () { it('can add authentication options and verify them after serialization', async function () { cluster = await MongoCluster.start({ - version: '6.x', + version: '8.x', topology: 'sharded', tmpDir, secondaries: 1, diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index 1b390ba1..b5add8be 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -238,6 +238,7 @@ export class MongoCluster extends EventEmitter { } isClosed(): boolean { + // Return true if and only if there are no running sub-clusters/servers // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const _ of this.children()) return true; return true; From 04249edc913273f4687b9426a61dee2da77de9e4 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 18 Nov 2025 14:12:35 +0100 Subject: [PATCH 16/22] fixup: version assertions --- .../mongodb-runner/src/mongocluster.spec.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/mongodb-runner/src/mongocluster.spec.ts b/packages/mongodb-runner/src/mongocluster.spec.ts index 73fce638..42087de6 100644 --- a/packages/mongodb-runner/src/mongocluster.spec.ts +++ b/packages/mongodb-runner/src/mongocluster.spec.ts @@ -95,7 +95,7 @@ describe('MongoCluster', function () { tmpDir, }); expect(cluster.connectionString).to.be.a('string'); - expect(cluster.serverVersion).to.match(/^6\./); + expect(cluster.serverVersion).to.match(/^8\./); expect(cluster.serverVariant).to.equal('community'); const { ok } = await cluster.withClient(async (client) => { return await client.db('admin').command({ ping: 1 }); @@ -120,7 +120,7 @@ describe('MongoCluster', function () { tmpDir, }); expect(cluster.connectionString).to.be.a('string'); - expect(cluster.serverVersion).to.match(/^6\./); + expect(cluster.serverVersion).to.match(/^8\./); const hello = await cluster.withClient(async (client) => { return await client.db('admin').command({ hello: 1 }); }); @@ -136,7 +136,7 @@ describe('MongoCluster', function () { arbiters: 1, }); expect(cluster.connectionString).to.be.a('string'); - expect(cluster.serverVersion).to.match(/^6\./); + expect(cluster.serverVersion).to.match(/^8\./); const hello = await cluster.withClient(async (client) => { return await client.db('admin').command({ hello: 1 }); }); @@ -156,7 +156,7 @@ describe('MongoCluster', function () { logDir, }); expect(cluster.connectionString).to.be.a('string'); - expect(cluster.serverVersion).to.match(/^6\./); + expect(cluster.serverVersion).to.match(/^8\./); const hello = await cluster.withClient(async (client) => { return await client.db('admin').command({ hello: 1 }); }); @@ -186,7 +186,7 @@ describe('MongoCluster', function () { ], }); expect(cluster.connectionString).to.be.a('string'); - expect(cluster.serverVersion).to.match(/^6\./); + expect(cluster.serverVersion).to.match(/^8\./); expect(cluster.serverVariant).to.equal('community'); }); @@ -373,7 +373,7 @@ describe('MongoCluster', function () { }); expect(cluster.connectionString).to.be.a('string'); - expect(cluster.serverVersion).to.match(/^6\./); + expect(cluster.serverVersion).to.match(/^8\./); const hello = await cluster.withClient(async (client) => { return await client.db('admin').command({ hello: 1 }); }); @@ -416,7 +416,7 @@ describe('MongoCluster', function () { }); expect(cluster.connectionString).to.be.a('string'); - expect(cluster.serverVersion).to.match(/^6\./); + expect(cluster.serverVersion).to.match(/^8\./); const shards = cluster['shards']; expect(shards).to.have.lengthOf(2); @@ -457,7 +457,7 @@ describe('MongoCluster', function () { }); expect(cluster.connectionString).to.be.a('string'); - expect(cluster.serverVersion).to.match(/^6\./); + expect(cluster.serverVersion).to.match(/^8\./); const mongoses = cluster['servers']; expect(mongoses).to.have.lengthOf(2); @@ -513,7 +513,7 @@ describe('MongoCluster', function () { mongosArgs: [[], []], }); expect(cluster.connectionString).to.be.a('string'); - expect(cluster.serverVersion).to.match(/^6\./); + expect(cluster.serverVersion).to.match(/^8\./); expect(cluster.connectionString).to.include('testuser:testpass@'); await cluster.withClient(async (client) => { const result = await client From 4746fa07c35e076c57421e6e2c0562bfdd97eb54 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 18 Nov 2025 16:10:56 +0100 Subject: [PATCH 17/22] fixup: cr, export interfaces, fix auth test for 8.x --- packages/mongodb-runner/src/index.ts | 4 +++ packages/mongodb-runner/src/mongocluster.ts | 34 ++++++++++++++++++++- packages/mongodb-runner/src/mongoserver.ts | 13 +++++--- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/mongodb-runner/src/index.ts b/packages/mongodb-runner/src/index.ts index 49037e74..d59500c9 100644 --- a/packages/mongodb-runner/src/index.ts +++ b/packages/mongodb-runner/src/index.ts @@ -8,6 +8,10 @@ export { type MongoClusterEvents, MongoClusterOptions, MongoDBUserDoc, + RSMemberOptions as MongoClusterRSMemberOptions, + RSOptions as MongoClusterRSOptions, + CommonOptions as MongoClusterCommonOptions, + ShardedOptions as MongoClusterShardedOptions, } from './mongocluster'; export type { LogEntry } from './mongologreader'; export type { ConnectionString } from 'mongodb-connection-string-url'; diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index b5add8be..b9729d85 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -3,7 +3,12 @@ import { MongoServer } from './mongoserver'; import { ConnectionString } from 'mongodb-connection-string-url'; import type { DownloadOptions } from '@mongodb-js/mongodb-downloader'; import { downloadMongoDb } from '@mongodb-js/mongodb-downloader'; -import type { Document, MongoClientOptions, TagSet } from 'mongodb'; +import type { + Document, + MongoClientOptions, + TagSet, + WriteConcernSettings, +} from 'mongodb'; import { MongoClient } from 'mongodb'; import { sleep, @@ -470,6 +475,7 @@ export class MongoCluster extends EventEmitter { const admin = client.db('admin'); for (const user of this.users) { const { username, password, ...rest } = user; + debug('adding new user', { username, ...rest }, this.connectionString); await admin.command({ createUser: username, pwd: password, ...rest }); } }); @@ -507,6 +513,7 @@ export class MongoCluster extends EventEmitter { clientOptions: MongoClientOptions = {}, ): Promise> { const client = await MongoClient.connect(this.connectionString, { + ...this.getFullWriteConcernOptions(), ...this.defaultConnectionOptions, ...clientOptions, }); @@ -524,4 +531,29 @@ export class MongoCluster extends EventEmitter { unref(): void { for (const child of this.children()) child.unref(); } + + // Return maximal write concern options based on topology + getFullWriteConcernOptions(): { writeConcern?: WriteConcernSettings } { + switch (this.topology) { + case 'standalone': + return {}; + case 'replset': + return { writeConcern: { w: this.servers.length, j: true } }; + case 'sharded': + return { + writeConcern: { + w: Math.min( + ...this.shards + .map((s) => s.getFullWriteConcernOptions().writeConcern?.w) + .filter((w) => typeof w === 'number'), + ), + j: true, + }, + }; + default: + throw new Error( + `Not implemented for topology ${this.topology as string}`, + ); + } + } } diff --git a/packages/mongodb-runner/src/mongoserver.ts b/packages/mongodb-runner/src/mongoserver.ts index cb4082cc..daf1d181 100644 --- a/packages/mongodb-runner/src/mongoserver.ts +++ b/packages/mongodb-runner/src/mongoserver.ts @@ -289,17 +289,20 @@ export class MongoServer extends EventEmitter { async updateDefaultConnectionOptions( options: Partial, ): Promise { - const buildInfoError = await this._populateBuildInfo('restore-check', { - ...options, - }); - if (buildInfoError) { + let buildInfoError: Error | null = null; + for (let attempts = 0; attempts < 10; attempts++) { + buildInfoError = await this._populateBuildInfo('restore-check', { + ...options, + }); + if (!buildInfoError) break; debug( 'failed to get buildInfo when setting new options', buildInfoError, options, + this.connectionString, ); - throw buildInfoError; } + if (buildInfoError) throw buildInfoError; this.defaultConnectionOptions = { ...this.defaultConnectionOptions, ...options, From acfe91179bd091597b791c75a5baf49f778d6ae4 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 24 Nov 2025 23:22:36 +0100 Subject: [PATCH 18/22] fixup: support TLS on internal client out of the box --- package-lock.json | 780 +++++++++++++++--- packages/mongodb-runner/package.json | 5 +- .../mongodb-runner/src/mongocluster.spec.ts | 123 ++- packages/mongodb-runner/src/mongocluster.ts | 7 +- packages/mongodb-runner/src/mongoserver.ts | 2 + packages/mongodb-runner/src/tls-helpers.ts | 111 +++ packages/mongodb-runner/src/util.ts | 27 + 7 files changed, 928 insertions(+), 127 deletions(-) create mode 100644 packages/mongodb-runner/src/tls-helpers.ts diff --git a/package-lock.json b/package-lock.json index 3426513e..b9fc2ac0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8747,6 +8747,220 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz", + "integrity": "sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-cms/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.0.tgz", + "integrity": "sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.0.tgz", + "integrity": "sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.0.tgz", + "integrity": "sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.0.tgz", + "integrity": "sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.0.tgz", + "integrity": "sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pfx": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.0.tgz", + "integrity": "sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.0.tgz", + "integrity": "sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.0.tgz", + "integrity": "sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-x509/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/x509": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.2.tgz", + "integrity": "sha512-r2w1Hg6pODDs0zfAKHkSS5HLkOLSeburtcgwvlLLWWCixw+MmW3U6kD5ddyvc2Y2YdbGuVwCF2S2ASoU1cFAag==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@peculiar/x509/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@pkgjs/nv": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@pkgjs/nv/-/nv-0.2.1.tgz", @@ -11648,6 +11862,26 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/asn1js": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", + "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/asn1js/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/assert": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", @@ -13559,31 +13793,6 @@ "node": ">=18" } }, - "node_modules/data-urls/node_modules/tr46": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz", - "integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -19449,18 +19658,6 @@ "node": ">=6" } }, - "node_modules/jsdom/node_modules/tr46": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz", - "integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/jsdom/node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -19469,19 +19666,6 @@ "node": ">= 4.0.0" } }, - "node_modules/jsdom/node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -25512,6 +25696,30 @@ "node": ">=6" } }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvtsutils/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -26263,6 +26471,12 @@ "node": ">=8" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -28551,14 +28765,15 @@ } }, "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", "dependencies": { - "punycode": "^2.3.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/traverse": { @@ -28715,6 +28930,18 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/tuf-js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-1.1.7.tgz", @@ -29386,15 +29613,16 @@ } }, "node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", "dependencies": { - "tr46": "^4.1.1", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/which": { @@ -30990,9 +31218,10 @@ "@mongodb-js/mongodb-downloader": "^1.0.0", "@mongodb-js/oidc-mock-provider": "^0.12.0", "@mongodb-js/saslprep": "^1.3.2", + "@peculiar/x509": "^1.14.2", "debug": "^4.4.0", - "mongodb": "^6.9.0", - "mongodb-connection-string-url": "^3.0.0", + "mongodb": "^6.9.0 || ^7.0.0", + "mongodb-connection-string-url": "^3.0.0 || ^7.0.0", "yargs": "^17.7.2" }, "bin": { @@ -31020,6 +31249,24 @@ "typescript": "^5.0.4" } }, + "packages/mongodb-runner/node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "packages/mongodb-runner/node_modules/bson": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.0.0.tgz", + "integrity": "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, "packages/mongodb-runner/node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -31033,6 +31280,65 @@ "node": ">=12" } }, + "packages/mongodb-runner/node_modules/mongodb": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.0.0", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "packages/mongodb-runner/node_modules/mongodb-connection-string-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.0.tgz", + "integrity": "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, "packages/mongodb-runner/node_modules/typescript": { "version": "5.8.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", @@ -40396,6 +40702,217 @@ "node-gyp-build": "^4.3.0" } }, + "@peculiar/asn1-cms": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz", + "integrity": "sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==", + "requires": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, + "@peculiar/asn1-csr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.0.tgz", + "integrity": "sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==", + "requires": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, + "@peculiar/asn1-ecc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.0.tgz", + "integrity": "sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==", + "requires": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, + "@peculiar/asn1-pfx": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.0.tgz", + "integrity": "sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==", + "requires": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, + "@peculiar/asn1-pkcs8": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.0.tgz", + "integrity": "sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==", + "requires": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, + "@peculiar/asn1-pkcs9": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.0.tgz", + "integrity": "sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==", + "requires": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pfx": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, + "@peculiar/asn1-rsa": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.0.tgz", + "integrity": "sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==", + "requires": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, + "@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "requires": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, + "@peculiar/asn1-x509": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.0.tgz", + "integrity": "sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==", + "requires": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, + "@peculiar/asn1-x509-attr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.0.tgz", + "integrity": "sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==", + "requires": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, + "@peculiar/x509": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.2.tgz", + "integrity": "sha512-r2w1Hg6pODDs0zfAKHkSS5HLkOLSeburtcgwvlLLWWCixw+MmW3U6kD5ddyvc2Y2YdbGuVwCF2S2ASoU1cFAag==", + "requires": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, "@pkgjs/nv": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@pkgjs/nv/-/nv-0.2.1.tgz", @@ -42764,6 +43281,23 @@ "safer-buffer": "~2.1.0" } }, + "asn1js": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", + "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", + "requires": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, "assert": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", @@ -44182,25 +44716,6 @@ "requires": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" - }, - "dependencies": { - "tr46": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz", - "integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==", - "requires": { - "punycode": "^2.3.1" - } - }, - "whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "requires": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - } - } } }, "data-view-buffer": { @@ -48461,27 +48976,10 @@ "url-parse": "^1.5.3" } }, - "tr46": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz", - "integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==", - "requires": { - "punycode": "^2.3.1" - } - }, "universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" - }, - "whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "requires": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - } } } }, @@ -51053,6 +51551,7 @@ "@mongodb-js/prettier-config-devtools": "^1.0.2", "@mongodb-js/saslprep": "^1.3.2", "@mongodb-js/tsconfig-devtools": "^1.0.4", + "@peculiar/x509": "^1.14.2", "@types/chai": "^4.2.21", "@types/debug": "^4.1.8", "@types/mocha": "^9.1.1", @@ -51065,8 +51564,8 @@ "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.3", "mocha": "^8.4.0", - "mongodb": "^6.9.0", - "mongodb-connection-string-url": "^3.0.0", + "mongodb": "^6.9.0 || ^7.0.0", + "mongodb-connection-string-url": "^3.0.0 || ^7.0.0", "nyc": "^15.1.0", "prettier": "^3.5.3", "sinon": "^9.2.3", @@ -51074,6 +51573,19 @@ "yargs": "^17.7.2" }, "dependencies": { + "@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "requires": { + "@types/webidl-conversions": "*" + } + }, + "bson": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.0.0.tgz", + "integrity": "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw==" + }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -51084,6 +51596,25 @@ "wrap-ansi": "^7.0.0" } }, + "mongodb": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "requires": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.0.0", + "mongodb-connection-string-url": "^7.0.0" + } + }, + "mongodb-connection-string-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.0.tgz", + "integrity": "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og==", + "requires": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + } + }, "typescript": { "version": "5.8.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", @@ -53339,6 +53870,26 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, + "pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "requires": { + "tslib": "^2.8.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, + "pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==" + }, "qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -53884,6 +54435,11 @@ "strip-indent": "^3.0.0" } }, + "reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, "reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -55605,11 +56161,11 @@ } }, "tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "requires": { - "punycode": "^2.3.0" + "punycode": "^2.3.1" } }, "traverse": { @@ -55719,6 +56275,14 @@ "tslib": "^1.8.1" } }, + "tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "requires": { + "tslib": "^1.9.3" + } + }, "tuf-js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-1.1.7.tgz", @@ -56200,11 +56764,11 @@ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==" }, "whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "requires": { - "tr46": "^4.1.1", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, diff --git a/packages/mongodb-runner/package.json b/packages/mongodb-runner/package.json index 5025c251..9c78c72c 100644 --- a/packages/mongodb-runner/package.json +++ b/packages/mongodb-runner/package.json @@ -59,9 +59,10 @@ "@mongodb-js/mongodb-downloader": "^1.0.0", "@mongodb-js/oidc-mock-provider": "^0.12.0", "@mongodb-js/saslprep": "^1.3.2", + "@peculiar/x509": "^1.14.2", "debug": "^4.4.0", - "mongodb": "^6.9.0", - "mongodb-connection-string-url": "^3.0.0", + "mongodb": "^6.9.0 || ^7.0.0", + "mongodb-connection-string-url": "^3.0.0 || ^7.0.0", "yargs": "^17.7.2" }, "devDependencies": { diff --git a/packages/mongodb-runner/src/mongocluster.spec.ts b/packages/mongodb-runner/src/mongocluster.spec.ts index 42087de6..f8c85272 100644 --- a/packages/mongodb-runner/src/mongocluster.spec.ts +++ b/packages/mongodb-runner/src/mongocluster.spec.ts @@ -6,6 +6,7 @@ import os from 'os'; import createDebug from 'debug'; import sinon from 'sinon'; import type { LogEntry } from './mongologreader'; +import type { MongoClientOptions } from 'mongodb'; if (process.env.CI) { createDebug.enable('mongodb-runner,mongodb-downloader'); @@ -17,6 +18,12 @@ const twoIfNotWindowsCI = // These are from the testing/certificates directory of mongosh const SERVER_KEY = 'server.bundle.pem'; const CA_CERT = 'ca.crt'; +const SHORT_TIMEOUTS: Partial = { + serverSelectionTimeoutMS: 500, + connectTimeoutMS: 500, + socketTimeoutMS: 500, + timeoutMS: 500, +}; describe('MongoCluster', function () { this.timeout(1_000_000); // Downloading Windows binaries can take a very long time... @@ -171,23 +178,107 @@ describe('MongoCluster', function () { ).to.equal(1); }); - it('can spawn a 8.x standalone mongod with TLS enabled and get build info', async function () { - cluster = await MongoCluster.start({ - version: '8.x', - topology: 'standalone', - tmpDir, - args: [ - '--tlsMode', - 'requireTLS', - '--tlsCertificateKeyFile', - path.resolve(__dirname, '..', 'test', 'fixtures', SERVER_KEY), - '--tlsCAFile', - path.resolve(__dirname, '..', 'test', 'fixtures', CA_CERT), - ], + context('TLS', function () { + it('can spawn a 8.x standalone mongod with TLS enabled and get build info (automatically added client key)', async function () { + cluster = await MongoCluster.start({ + version: '8.x', + topology: 'standalone', + tmpDir, + args: [ + '--tlsMode', + 'requireTLS', + '--tlsCertificateKeyFile', + path.resolve(__dirname, '..', 'test', 'fixtures', SERVER_KEY), + '--tlsCAFile', + path.resolve(__dirname, '..', 'test', 'fixtures', CA_CERT), + ], + }); + expect(cluster.connectionString).to.be.a('string'); + expect(cluster.connectionString).to.include( + 'tls=true&tlsCertificateKeyFile=', + ); + expect(cluster.serverVersion).to.match(/^8\./); + expect(cluster.serverVariant).to.equal('community'); + + await cluster.withClient(async (client) => { + expect( + path.basename(client.options.tlsCertificateKeyFile ?? ''), + ).to.include('mongodb-runner-client-'); + const buildInfo = await client.db('admin').command({ buildInfo: 1 }); + expect(buildInfo.version).to.be.a('string'); + }); + }); + + it('can spawn a 8.x standalone mongod with TLS enabled and get build info (no extra client key)', async function () { + cluster = await MongoCluster.start({ + version: '8.x', + topology: 'standalone', + tmpDir, + tlsAddClientKey: false, + internalClientOptions: { ...SHORT_TIMEOUTS }, + args: [ + '--tlsMode', + 'requireTLS', + '--tlsCertificateKeyFile', + path.resolve(__dirname, '..', 'test', 'fixtures', SERVER_KEY), + '--tlsCAFile', + path.resolve(__dirname, '..', 'test', 'fixtures', CA_CERT), + ], + }); + expect(cluster.connectionString).to.be.a('string'); + expect(cluster.connectionString).not.to.include('tls='); + expect(cluster.connectionString).not.to.include('tlsCertificateKeyFile='); + expect(cluster.serverVersion).to.match(/^8\./); + expect(cluster.serverVariant).to.equal('community'); + + try { + await cluster.withClient(() => {}, { ...SHORT_TIMEOUTS }); + expect.fail('missed exception'); + } catch (err: any) { + expect(err.name).to.equal('MongoServerSelectionError'); + } + }); + + it('can spawn a 8.x standalone mongod with TLS enabled and get build info (explicit client cert)', async function () { + cluster = await MongoCluster.start({ + version: '8.x', + topology: 'standalone', + tmpDir, + internalClientOptions: { + tls: true, + tlsCAFile: path.resolve(__dirname, '..', 'test', 'fixtures', CA_CERT), + tlsCertificateKeyFile: path.resolve( + __dirname, + '..', + 'test', + 'fixtures', + SERVER_KEY, + ), + tlsAllowInvalidCertificates: true, + }, + args: [ + '--tlsMode', + 'requireTLS', + '--tlsCertificateKeyFile', + path.resolve(__dirname, '..', 'test', 'fixtures', SERVER_KEY), + '--tlsCAFile', + path.resolve(__dirname, '..', 'test', 'fixtures', CA_CERT), + ], + }); + expect(cluster.connectionString).to.be.a('string'); + expect(cluster.connectionString).to.include('tls=true&tlsCAFile='); + expect(cluster.connectionString).to.include('tlsCertificateKeyFile='); + expect(cluster.serverVersion).to.match(/^8\./); + expect(cluster.serverVariant).to.equal('community'); + + await cluster.withClient(async (client) => { + expect( + path.basename(client.options.tlsCertificateKeyFile ?? ''), + ).to.equal(SERVER_KEY); + const buildInfo = await client.db('admin').command({ buildInfo: 1 }); + expect(buildInfo.version).to.be.a('string'); + }); }); - expect(cluster.connectionString).to.be.a('string'); - expect(cluster.serverVersion).to.match(/^8\./); - expect(cluster.serverVariant).to.equal('community'); }); context('on Linux', function () { diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index b9729d85..b6654794 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -22,6 +22,7 @@ import { import { OIDCMockProviderProcess } from './oidc'; import { EventEmitter } from 'events'; import assert from 'assert'; +import { handleTLSClientKeyOptions } from './tls-helpers'; export interface MongoDBUserDoc { username: string; @@ -45,6 +46,7 @@ export interface CommonOptions { version?: string; users?: MongoDBUserDoc[]; + tlsAddClientKey?: boolean; topology: 'standalone' | 'replset' | 'sharded'; } @@ -76,7 +78,7 @@ export type ShardedOptions = { export type MongoClusterOptions = Pick< MongoServerOptions, - 'logDir' | 'tmpDir' | 'args' | 'binDir' | 'docker' + 'logDir' | 'tmpDir' | 'args' | 'binDir' | 'docker' | 'internalClientOptions' > & CommonOptions & RSOptions & @@ -298,9 +300,12 @@ export class MongoCluster extends EventEmitter { static async start({ ...options }: MongoClusterOptions): Promise { + options = { ...options, ...(await handleTLSClientKeyOptions(options)) }; + const cluster = new MongoCluster(); cluster.topology = options.topology; cluster.users = options.users ?? []; + cluster.defaultConnectionOptions = { ...options.internalClientOptions }; if (!options.binDir) { options.binDir = await this.downloadMongoDb( options.downloadDir ?? options.tmpDir, diff --git a/packages/mongodb-runner/src/mongoserver.ts b/packages/mongodb-runner/src/mongoserver.ts index daf1d181..6d4f6457 100644 --- a/packages/mongodb-runner/src/mongoserver.ts +++ b/packages/mongodb-runner/src/mongoserver.ts @@ -29,6 +29,7 @@ export interface MongoServerOptions { logDir?: string; // If set, pipe log file output through here. args?: string[]; // May or may not contain --port docker?: string | string[]; // Image or docker options + internalClientOptions?: Partial; } interface SerializedServerProperties { @@ -154,6 +155,7 @@ export class MongoServer extends EventEmitter { ...options }: MongoServerOptions): Promise { const srv = new MongoServer(); + srv.defaultConnectionOptions = { ...options.internalClientOptions }; if (!options.docker) { const dbPath = path.join(options.tmpDir, `db-${srv.uuid}`); await fs.mkdir(dbPath, { recursive: true }); diff --git a/packages/mongodb-runner/src/tls-helpers.ts b/packages/mongodb-runner/src/tls-helpers.ts new file mode 100644 index 00000000..dd19985c --- /dev/null +++ b/packages/mongodb-runner/src/tls-helpers.ts @@ -0,0 +1,111 @@ +import * as x509 from '@peculiar/x509'; +import { webcrypto } from 'crypto'; +import { uuid } from './util'; +import path from 'path'; +import { writeFile, readFile } from 'fs/promises'; +import type { MongoClientOptions } from 'mongodb'; +x509.cryptoProvider.set(webcrypto as typeof crypto); + +export interface TLSClientOptions { + tlsAddClientKey?: boolean; + args?: string[]; + tmpDir: string; + internalClientOptions?: Partial; +} + +export async function handleTLSClientKeyOptions({ + tlsAddClientKey, + args: [...args] = [], + tmpDir, + internalClientOptions = {}, +}: TLSClientOptions): Promise> { + const existingTLSCAOptionIndex = args.findIndex((arg) => + arg.match(/^--tls(Cluster)?CAFile(=|$)/), + ); + + if (tlsAddClientKey === false) return {}; + if (tlsAddClientKey !== true && existingTLSCAOptionIndex === -1) return {}; + if (tlsAddClientKey !== true && internalClientOptions.tlsCertificateKeyFile) + return {}; + + const alg: RsaHashedKeyGenParams = { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256', + publicExponent: new Uint8Array([1, 0, 1]), + modulusLength: 2048, + }; + const now = Date.now(); + const keys = await webcrypto.subtle.generateKey(alg, true, [ + 'sign', + 'verify', + ]); + const cert = await x509.X509CertificateGenerator.createSelfSigned({ + name: 'O=MongoDB, OU=MongoDBRunnerCA, CN=MongoDBRunnerCA', + notBefore: new Date(now - 1000 * 60 * 60), + notAfter: new Date(now + 1000 * 60 * 60 * 24 * 365 * 10), + signingAlgorithm: alg, + keys, + extensions: [ + await x509.SubjectKeyIdentifierExtension.create(keys.publicKey), + ], + }); + const clientPEMContent = Buffer.from(cert.toString('pem') + '\n'); + + const existingTLSCAOptionHasValue = + args[existingTLSCAOptionIndex].includes('='); + const existingTLSCAOption = args[existingTLSCAOptionIndex].split('=')[0]; + const existingTLSCAOptionValue = existingTLSCAOptionHasValue + ? args[existingTLSCAOptionIndex].split('=')[1] + : args[existingTLSCAOptionIndex + 1]; + + const id = uuid(); + const clientPEM = path.join(tmpDir, `mongodb-runner-client-${id}.pem`); + const caPEM = path.join(tmpDir, `mongodb-runner-ca-${id}.pem`); + + await Promise.all([ + (async () => { + await writeFile( + clientPEM, + Buffer.concat([ + clientPEMContent, + Buffer.from( + pkcs8ToPEM( + await webcrypto.subtle.exportKey('pkcs8', keys.privateKey), + ), + ), + ]), + ); + })(), + (async () => { + await writeFile( + caPEM, + Buffer.concat([ + clientPEMContent, + await readFile(existingTLSCAOptionValue), + ]), + ); + })(), + ]); + + args.splice( + existingTLSCAOptionIndex, + existingTLSCAOptionHasValue ? 1 : 2, + `${existingTLSCAOption}=${caPEM}`, + ); + + return { + args, + tlsAddClientKey: false, + internalClientOptions: { + tlsCertificateKeyFile: clientPEM, + tlsAllowInvalidCertificates: true, + tls: true, + ...internalClientOptions, + }, + }; +} + +function pkcs8ToPEM(pkcs8Buffer: ArrayBuffer): string { + const b64 = Buffer.from(pkcs8Buffer).toString('base64'); + return `-----BEGIN PRIVATE KEY-----\n${b64.match(/.{1,64}/g)?.join('\n') || ''}\n-----END PRIVATE KEY-----\n`; +} diff --git a/packages/mongodb-runner/src/util.ts b/packages/mongodb-runner/src/util.ts index 4b49df77..a7900731 100644 --- a/packages/mongodb-runner/src/util.ts +++ b/packages/mongodb-runner/src/util.ts @@ -58,6 +58,33 @@ export function makeConnectionString( if (replSetName) { cs.typedSearchParams().set('replicaSet', replSetName); } + if (defaultConnectionOptions.tls) { + cs.typedSearchParams().set('tls', 'true'); + } + if (defaultConnectionOptions.tlsCAFile) { + cs.typedSearchParams().set( + 'tlsCAFile', + defaultConnectionOptions.tlsCAFile, + ); + } + if (defaultConnectionOptions.tlsCertificateKeyFile) { + cs.typedSearchParams().set( + 'tlsCertificateKeyFile', + defaultConnectionOptions.tlsCertificateKeyFile, + ); + } + if (defaultConnectionOptions.tlsCertificateKeyFilePassword) { + cs.typedSearchParams().set( + 'tlsCertificateKeyFilePassword', + defaultConnectionOptions.tlsCertificateKeyFilePassword, + ); + } + if (defaultConnectionOptions.tlsAllowInvalidCertificates) { + cs.typedSearchParams().set( + 'tlsAllowInvalidCertificates', + 'true', + ); + } if (defaultConnectionOptions.auth?.username) { cs.username = defaultConnectionOptions.auth.username; } From 401163c02fd0200aac2745f6fec098b4b90cdfb3 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 24 Nov 2025 23:51:09 +0100 Subject: [PATCH 19/22] fixup: full flexibility for individual shards --- .../mongodb-runner/src/mongocluster.spec.ts | 6 +- packages/mongodb-runner/src/mongocluster.ts | 74 ++++++++----------- 2 files changed, 35 insertions(+), 45 deletions(-) diff --git a/packages/mongodb-runner/src/mongocluster.spec.ts b/packages/mongodb-runner/src/mongocluster.spec.ts index f8c85272..d5c05688 100644 --- a/packages/mongodb-runner/src/mongocluster.spec.ts +++ b/packages/mongodb-runner/src/mongocluster.spec.ts @@ -500,9 +500,9 @@ describe('MongoCluster', function () { topology: 'sharded', tmpDir, secondaries: 0, - shardArgs: [ - ['--setParameter', 'cursorTimeoutMillis=60000'], - ['--setParameter', 'cursorTimeoutMillis=50000'], + shards: [ + { args: ['--setParameter', 'cursorTimeoutMillis=60000'] }, + { args: ['--setParameter', 'cursorTimeoutMillis=50000'] }, ], }); diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index b6654794..cff3d883 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -51,30 +51,16 @@ export interface CommonOptions { topology: 'standalone' | 'replset' | 'sharded'; } -export type RSOptions = - | { - arbiters?: number; - secondaries?: number; - rsMembers?: never; - } - | { - arbiters?: never; - secondaries?: never; - rsMembers: RSMemberOptions[]; - }; +export type RSOptions = { + arbiters?: number; + secondaries?: number; + rsMembers?: RSMemberOptions[]; +}; export type ShardedOptions = { mongosArgs?: string[][]; -} & ( - | { - shards?: number; - shardArgs?: never; - } - | { - shards?: never; - shardArgs?: string[][]; - } -); + shards?: number | Partial[]; +}; export type MongoClusterOptions = Pick< MongoServerOptions, @@ -167,25 +153,29 @@ function processRSMembers(options: MongoClusterOptions): { } function processShardOptions(options: MongoClusterOptions): { - shardArgs: string[][]; + shards: Partial[]; mongosArgs: string[][]; } { - const { - shardArgs = range((options.shards ?? 3) + 1).map(() => []), - mongosArgs = [[]], - args = [], - } = options; + const shards: Partial[] = + typeof options.shards === 'number' || !options.shards + ? range((options.shards ?? 3) + 1).map( + () => ({}) as Partial, + ) + : options.shards; + const { mongosArgs = [[]], args = [] } = options; return { - shardArgs: shardArgs.map((perShardArgs, i) => [ - ...removePortArg(args), - ...perShardArgs, - ...(perShardArgs.includes('--configsvr') || - perShardArgs.includes('--shardsvr') - ? [] - : i === 0 - ? ['--configsvr'] - : ['--shardsvr']), - ]), + shards: shards.map(({ args = [], ...perShardOpts }, i) => ({ + ...perShardOpts, + args: [ + ...removePortArg(args), + ...args, + ...(args.includes('--configsvr') || args.includes('--shardsvr') + ? [] + : i === 0 + ? ['--configsvr'] + : ['--shardsvr']), + ], + })), mongosArgs: mongosArgs.map((perMongosArgs, i) => [ ...(i === 0 && !hasPortArg(perMongosArgs) ? args : removePortArg(args)), ...perMongosArgs, @@ -414,14 +404,14 @@ export class MongoCluster extends EventEmitter { } }); } else if (options.topology === 'sharded') { - const { shardArgs, mongosArgs } = processShardOptions(options); - debug('starting config server and shard servers', shardArgs); + const { shards, mongosArgs } = processShardOptions(options); + debug('starting config server and shard servers', shards); const allShards = await Promise.all( - shardArgs.map(async (args) => { - const isConfig = args.includes('--configsvr'); + shards.map(async (s) => { + const isConfig = s.args?.includes('--configsvr'); const cluster = await MongoCluster.start({ ...options, - args, + ...s, topology: 'replset', users: isConfig ? undefined : options.users, // users go on the mongos/config server only for the config set }); From 42cc373d7827ca997023782430e54724e89c70d2 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 24 Nov 2025 23:57:12 +0100 Subject: [PATCH 20/22] chore: add documentation --- packages/mongodb-runner/src/mongocluster.ts | 73 +++++++++++++++++++++ packages/mongodb-runner/src/mongoserver.ts | 20 ++++-- 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index cff3d883..2afc9715 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -24,41 +24,114 @@ import { EventEmitter } from 'events'; import assert from 'assert'; import { handleTLSClientKeyOptions } from './tls-helpers'; +/** + * Description of a MongoDB user account that will be created in a test cluster. + */ export interface MongoDBUserDoc { + /** + * SCRAM-SHA-256 username. + */ username: string; + /** + * SCRAM-SHA-256 password. + */ password: string; + /** + * Additional metadata for a given user. + */ customData?: Document; + /** + * Roles to assign to the user. + */ roles: ({ role: string; db?: string } | string)[]; + /** + * Additional fields may be included as per the `createUser` command. + */ + [key: string]: unknown; } +/** Describe the individual members of a replica set */ export interface RSMemberOptions { + /** + * Tags to assign to the member, in the format expected by the Node.js driver. + */ tags?: TagSet; + /** + * Priority of the member. If none is specified, one member will be given priority 1 + * and all others priority 0. The mongodb-runner package assumes that the highest priority + * member will become primary. + */ priority?: number; + /** + * Additional arguments for the member. + */ args?: string[]; + /** + * Whether the member is an arbiter. + */ arbiterOnly?: boolean; } +/** + * Shared options for all cluster topologies. + */ export interface CommonOptions { + /** + * Directory where server binaries will be downloaded and stored. + */ downloadDir?: string; + /** + * Various options to control the download of MongoDB binaries. + */ downloadOptions?: DownloadOptions; + /** + * OIDC mock provider command line (e.g. '--port=0' or full path to binary). + * If provided, an OIDC mock provider will be started alongside the cluster, + * and the necessary parameters to connect to it will be added to the + * cluster's mongod/mongos processes. + */ oidc?: string; + /** + * MongoDB server version to download and use (e.g. '6.0.3', '8.x-enterprise', 'latest-alpha', etc.) + */ version?: string; + /** + * User accounts to create after starting the cluster. + */ users?: MongoDBUserDoc[]; + + /** + * Whether to automatically add an additional TLS client certificate key file + * to the cluster nodes based on whether TLS configuration was detected. + * + * Adding this is required in order for authentication to work when TLS is enabled. + */ tlsAddClientKey?: boolean; + /** + * Topology of the cluster. + */ topology: 'standalone' | 'replset' | 'sharded'; } +/** + * Options specific to replica set clusters. + */ export type RSOptions = { + /** Number of arbiters to create (default: 0) */ arbiters?: number; + /** Number of secondary nodes to create (default: 2) */ secondaries?: number; + /** Explicitly specify replica set members. If set, `arbiters` and `secondaries` will be ignored. */ rsMembers?: RSMemberOptions[]; }; export type ShardedOptions = { + /** Arguments to pass to each mongos instance. */ mongosArgs?: string[][]; + /** Number of shards to create or explicit shard configurations. */ shards?: number | Partial[]; }; diff --git a/packages/mongodb-runner/src/mongoserver.ts b/packages/mongodb-runner/src/mongoserver.ts index 6d4f6457..7e7fab1c 100644 --- a/packages/mongodb-runner/src/mongoserver.ts +++ b/packages/mongodb-runner/src/mongoserver.ts @@ -22,13 +22,23 @@ import { makeConnectionString, } from './util'; +/** + * Options for starting a MongoDB server process. + */ export interface MongoServerOptions { + /** Directory where server binaries are located. */ binDir?: string; - binary: string; // 'mongod', 'mongos', etc. - tmpDir: string; // Stores e.g. database contents - logDir?: string; // If set, pipe log file output through here. - args?: string[]; // May or may not contain --port - docker?: string | string[]; // Image or docker options + /** The MongoDB binary to run, e.g., 'mongod', 'mongos', etc. */ + binary: string; + /** Directory for temporary files, e.g., database contents */ + tmpDir: string; + /** If set, log file output will be piped through here. */ + logDir?: string; + /** Arguments to pass to the MongoDB binary. May or may not contain --port */ + args?: string[]; + /** Docker image or options to run the MongoDB binary in a container. */ + docker?: string | string[]; + /** Internal options for the MongoDB client used by this server instance. */ internalClientOptions?: Partial; } From d919fe94c48b1957ac2534f02cc1eade8dee44ef Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 25 Nov 2025 00:02:35 +0100 Subject: [PATCH 21/22] fixup: bump to Node.js 22.x in CI --- .github/workflows/bump-packages.yaml | 2 +- .github/workflows/check-test.yaml | 2 +- .github/workflows/publish-packages.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bump-packages.yaml b/.github/workflows/bump-packages.yaml index 16f81e0c..333eaa27 100644 --- a/.github/workflows/bump-packages.yaml +++ b/.github/workflows/bump-packages.yaml @@ -33,7 +33,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x cache: "npm" - name: Install npm diff --git a/.github/workflows/check-test.yaml b/.github/workflows/check-test.yaml index 849ae1f4..bac6abb3 100644 --- a/.github/workflows/check-test.yaml +++ b/.github/workflows/check-test.yaml @@ -56,7 +56,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x cache: "npm" - name: Use python@3.11 diff --git a/.github/workflows/publish-packages.yaml b/.github/workflows/publish-packages.yaml index 4ec48081..ccf35606 100644 --- a/.github/workflows/publish-packages.yaml +++ b/.github/workflows/publish-packages.yaml @@ -40,7 +40,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x cache: "npm" - name: Install npm From 41b1b322edf85f074426471ec93a74569066cd70 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 25 Nov 2025 11:24:23 +0100 Subject: [PATCH 22/22] fixup: update tests --- .../mongodb-runner/src/mongocluster.spec.ts | 30 ++++++++++--------- packages/mongodb-runner/src/util.ts | 22 ++++++++++++++ 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/mongodb-runner/src/mongocluster.spec.ts b/packages/mongodb-runner/src/mongocluster.spec.ts index d5c05688..1634dc8b 100644 --- a/packages/mongodb-runner/src/mongocluster.spec.ts +++ b/packages/mongodb-runner/src/mongocluster.spec.ts @@ -289,12 +289,12 @@ describe('MongoCluster', function () { // This is the easiest way to ensure that MongoServer can handle the // pre-4.4 log format (because in the devtools-shared CI, we only // test ubuntu-latest). - it('can spawn a 4.0.x replset using docker', async function () { + it('can spawn a 4.2.x replset using docker', async function () { cluster = await MongoCluster.start({ - version: '4.0.x', + version: '4.2.x', topology: 'replset', tmpDir, - docker: 'mongo:4.0', + docker: 'mongo:4.2', downloadOptions: { distro: 'ubuntu1604', }, @@ -307,12 +307,12 @@ describe('MongoCluster', function () { expect(+hello.passives.length + +hello.hosts.length).to.equal(3); }); - it('can spawn a 4.0.x sharded env using docker', async function () { + it('can spawn a 4.2.x sharded env using docker', async function () { cluster = await MongoCluster.start({ - version: '4.0.x', + version: '4.2.x', topology: 'sharded', tmpDir, - docker: 'mongo:4.0', + docker: 'mongo:4.2', shards: 1, secondaries: 0, downloadOptions: { @@ -327,9 +327,9 @@ describe('MongoCluster', function () { expect(hello.msg).to.equal('isdbgrid'); }); - it('can spawn a 4.0.x standalone mongod with TLS enabled and get build info', async function () { + it('can spawn a 4.2.x standalone mongod with TLS enabled and get build info', async function () { cluster = await MongoCluster.start({ - version: '4.0.x', + version: '4.2.x', topology: 'standalone', tmpDir, args: [ @@ -342,7 +342,7 @@ describe('MongoCluster', function () { ], docker: [ `--volume=${path.resolve(__dirname, '..')}:/projectroot:ro`, - 'mongo:4.0', + 'mongo:4.2', ], downloadOptions: { distro: 'ubuntu1604', @@ -580,11 +580,13 @@ describe('MongoCluster', function () { [50000, 'isdbgrid', false], ]); - const mongosList = await cluster.withClient( - async (client) => - await client.db('config').collection('mongos').find().toArray(), - ); - expect(mongosList).to.have.lengthOf(2); + await eventually(async () => { + const mongosList = await cluster.withClient( + async (client) => + await client.db('config').collection('mongos').find().toArray(), + ); + expect(mongosList).to.have.lengthOf(2); + }); }); it('can add authentication options and verify them after serialization', async function () { diff --git a/packages/mongodb-runner/src/util.ts b/packages/mongodb-runner/src/util.ts index a7900731..55618657 100644 --- a/packages/mongodb-runner/src/util.ts +++ b/packages/mongodb-runner/src/util.ts @@ -93,3 +93,25 @@ export function makeConnectionString( } return cs.toString(); } + +export async function eventually( + fn: () => Promise | void, + { + intervalMs = 100, + timeoutMs = 5000, + }: { intervalMs?: number; timeoutMs?: number } = {}, +): Promise { + const startTime = Date.now(); + // eslint-disable-next-line no-constant-condition + while (true) { + try { + await fn(); + } catch (err) { + if (Date.now() - startTime > timeoutMs) { + throw err; + } else { + await sleep(intervalMs); + } + } + } +}