Skip to content

Commit fea6ceb

Browse files
committed
add tests, fix behaviors
1 parent d159b3d commit fea6ceb

File tree

4 files changed

+272
-41
lines changed

4 files changed

+272
-41
lines changed

packages/mongodb-runner/src/mongocluster.spec.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,4 +345,172 @@ describe('MongoCluster', function () {
345345
baddoc: 1,
346346
});
347347
});
348+
349+
it('can pass custom arguments for replica set members', async function () {
350+
cluster = await MongoCluster.start({
351+
version: '6.x',
352+
topology: 'replset',
353+
tmpDir,
354+
rsMembers: [
355+
{ args: ['--setParameter', 'cursorTimeoutMillis=60000'] },
356+
{ args: ['--setParameter', 'cursorTimeoutMillis=50000'] },
357+
],
358+
});
359+
360+
expect(cluster.connectionString).to.be.a('string');
361+
expect(cluster.serverVersion).to.match(/^6\./);
362+
const hello = await cluster.withClient(async (client) => {
363+
return await client.db('admin').command({ hello: 1 });
364+
});
365+
expect(hello.hosts).to.have.lengthOf(1);
366+
expect(hello.passives).to.have.lengthOf(1);
367+
368+
const servers = cluster['servers'];
369+
expect(servers).to.have.lengthOf(2);
370+
const values = await Promise.all(
371+
servers.map((srv) =>
372+
srv.withClient(async (client) => {
373+
return await Promise.all([
374+
client
375+
.db('admin')
376+
.command({ getParameter: 1, cursorTimeoutMillis: 1 }),
377+
client.db('admin').command({ hello: 1 }),
378+
]);
379+
}),
380+
),
381+
);
382+
383+
expect(
384+
values.map((v) => [v[0].cursorTimeoutMillis, v[1].isWritablePrimary]),
385+
).to.deep.equal([
386+
[60000, true],
387+
[50000, false],
388+
]);
389+
});
390+
391+
it('can pass custom arguments for shards', async function () {
392+
cluster = await MongoCluster.start({
393+
version: '6.x',
394+
topology: 'sharded',
395+
tmpDir,
396+
secondaries: 0,
397+
shardArgs: [
398+
['--setParameter', 'cursorTimeoutMillis=60000'],
399+
['--setParameter', 'cursorTimeoutMillis=50000'],
400+
],
401+
});
402+
403+
expect(cluster.connectionString).to.be.a('string');
404+
expect(cluster.serverVersion).to.match(/^6\./);
405+
406+
const shards = cluster['shards'];
407+
expect(shards).to.have.lengthOf(2);
408+
const values = await Promise.all(
409+
shards.map((srv) =>
410+
srv.withClient(async (client) => {
411+
return await Promise.all([
412+
client
413+
.db('admin')
414+
.command({ getParameter: 1, cursorTimeoutMillis: 1 }),
415+
client.db('admin').command({ hello: 1 }),
416+
]);
417+
}),
418+
),
419+
);
420+
421+
expect(
422+
values.map((v) => [
423+
v[0].cursorTimeoutMillis,
424+
v[1].setName === values[0][1].setName,
425+
]),
426+
).to.deep.equal([
427+
[60000, true],
428+
[50000, false],
429+
]);
430+
});
431+
432+
it('can pass custom arguments for mongoses', async function () {
433+
cluster = await MongoCluster.start({
434+
version: '6.x',
435+
topology: 'sharded',
436+
tmpDir,
437+
secondaries: 0,
438+
mongosArgs: [
439+
['--setParameter', 'cursorTimeoutMillis=60000'],
440+
['--setParameter', 'cursorTimeoutMillis=50000'],
441+
],
442+
});
443+
444+
expect(cluster.connectionString).to.be.a('string');
445+
expect(cluster.serverVersion).to.match(/^6\./);
446+
447+
const mongoses = cluster['servers'];
448+
expect(mongoses).to.have.lengthOf(2);
449+
const values = await Promise.all(
450+
mongoses.map((srv) =>
451+
srv.withClient(async (client) => {
452+
return await Promise.all([
453+
client
454+
.db('admin')
455+
.command({ getParameter: 1, cursorTimeoutMillis: 1 }),
456+
client.db('admin').command({ hello: 1 }),
457+
]);
458+
}),
459+
),
460+
);
461+
462+
const processIdForMongos = (v: any) =>
463+
v[1].topologyVersion.processId.toHexString();
464+
expect(
465+
values.map((v) => [
466+
v[0].cursorTimeoutMillis,
467+
v[1].msg,
468+
processIdForMongos(v) === processIdForMongos(values[0]),
469+
]),
470+
).to.deep.equal([
471+
[60000, 'isdbgrid', true],
472+
[50000, 'isdbgrid', false],
473+
]);
474+
});
475+
476+
it('can add authentication options and verify them after serialization', async function () {
477+
cluster = await MongoCluster.start({
478+
version: '6.x',
479+
topology: 'sharded',
480+
tmpDir,
481+
secondaries: 1,
482+
shards: 1,
483+
users: [
484+
{
485+
username: 'testuser',
486+
password: 'testpass',
487+
roles: [{ role: 'readWriteAnyDatabase', db: 'admin' }],
488+
},
489+
],
490+
mongosArgs: [[], []],
491+
});
492+
expect(cluster.connectionString).to.be.a('string');
493+
expect(cluster.serverVersion).to.match(/^6\./);
494+
expect(cluster.connectionString).to.include('testuser:testpass@');
495+
await cluster.withClient(async (client) => {
496+
const result = await client
497+
.db('test')
498+
.collection('test')
499+
.insertOne({ foo: 42 });
500+
expect(result.insertedId).to.exist;
501+
});
502+
503+
cluster = await MongoCluster.deserialize(cluster.serialize());
504+
expect(cluster.connectionString).to.include('testuser:testpass@');
505+
const [doc, status] = await cluster.withClient(async (client) => {
506+
return Promise.all([
507+
client.db('test').collection('test').findOne({ foo: 42 }),
508+
client.db('admin').command({ connectionStatus: 1 }),
509+
] as const);
510+
});
511+
expect(doc?.foo).to.equal(42);
512+
expect(status.authInfo.authenticatedUsers).to.deep.equal([
513+
{ user: 'testuser', db: 'admin' },
514+
]);
515+
});
348516
});

packages/mongodb-runner/src/mongocluster.ts

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ import type { DownloadOptions } from '@mongodb-js/mongodb-downloader';
55
import { downloadMongoDb } from '@mongodb-js/mongodb-downloader';
66
import type { Document, MongoClientOptions, TagSet } from 'mongodb';
77
import { MongoClient } from 'mongodb';
8-
import { sleep, range, uuid, debug, jsonClone } from './util';
8+
import {
9+
sleep,
10+
range,
11+
uuid,
12+
debug,
13+
jsonClone,
14+
debugVerbose,
15+
makeConnectionString,
16+
} from './util';
917
import { OIDCMockProviderProcess } from './oidc';
1018
import { EventEmitter } from 'events';
1119
import assert from 'assert';
@@ -156,7 +164,7 @@ function processShardOptions(options: MongoClusterOptions): {
156164
mongosArgs: string[][];
157165
} {
158166
const {
159-
shardArgs = range(options.shards ?? 1).map(() => []),
167+
shardArgs = range((options.shards ?? 3) + 1).map(() => []),
160168
mongosArgs = [[]],
161169
args = [],
162170
} = options;
@@ -258,13 +266,11 @@ export class MongoCluster extends EventEmitter<MongoClusterEvents> {
258266
}
259267

260268
get connectionString(): string {
261-
const cs = new ConnectionString(`mongodb://${this.hostport}/`);
262-
if (this.replSetName)
263-
cs.typedSearchParams<MongoClientOptions>().set(
264-
'replicaSet',
265-
this.replSetName,
266-
);
267-
return cs.toString();
269+
return makeConnectionString(
270+
this.hostport,
271+
this.replSetName,
272+
this.defaultConnectionOptions,
273+
);
268274
}
269275

270276
get oidcIssuer(): string | undefined {
@@ -367,11 +373,12 @@ export class MongoCluster extends EventEmitter<MongoClusterEvents> {
367373
_id: i,
368374
host: srv.hostport,
369375
arbiterOnly: member.arbiterOnly ?? false,
370-
priority: member.priority ?? 1,
376+
priority: member.priority ?? (i === primaryIndex ? 1 : 0),
371377
tags: member.tags || {},
372378
};
373379
}),
374380
};
381+
debugVerbose('replSetInitiate:', rsConf);
375382
await client.db('admin').command({
376383
replSetInitiate: rsConf,
377384
});
@@ -398,48 +405,58 @@ export class MongoCluster extends EventEmitter<MongoClusterEvents> {
398405
} else if (options.topology === 'sharded') {
399406
const { shardArgs, mongosArgs } = processShardOptions(options);
400407
debug('starting config server and shard servers', shardArgs);
401-
const [configsvr, ...shardsvrs] = await Promise.all(
402-
shardArgs.map((args) => {
403-
return MongoCluster.start({
408+
const allShards = await Promise.all(
409+
shardArgs.map(async (args) => {
410+
const isConfig = args.includes('--configsvr');
411+
const cluster = await MongoCluster.start({
404412
...options,
405413
args,
406414
topology: 'replset',
415+
users: isConfig ? undefined : options.users, // users go on the mongos/config server only for the config set
407416
});
417+
return [cluster, isConfig] as const;
408418
}),
409419
);
420+
const configsvr = allShards.find(([, isConfig]) => isConfig)![0];
421+
const shardsvrs = allShards
422+
.filter(([, isConfig]) => !isConfig)
423+
.map(([shard]) => shard);
410424
cluster.shards.push(configsvr, ...shardsvrs);
411425

412-
for (let i = 0; i < mongosArgs.length; i++) {
413-
debug('starting mongos');
414-
const mongos = await MongoServer.start({
415-
...options,
416-
binary: 'mongos',
417-
args: [
418-
...(options.args ?? []),
419-
...mongosArgs[i],
420-
'--configdb',
421-
`${configsvr.replSetName!}/${configsvr.hostport}`,
422-
],
423-
});
424-
cluster.servers.push(mongos);
425-
await mongos.withClient(async (client) => {
426-
for (const shard of shardsvrs) {
427-
const shardSpec = `${shard.replSetName!}/${shard.hostport}`;
428-
debug('adding shard', shardSpec);
429-
await client.db('admin').command({
430-
addShard: shardSpec,
431-
});
432-
}
433-
debug('added shards');
434-
});
435-
}
426+
const mongosServers: MongoServer[] = await Promise.all(
427+
mongosArgs.map(async (args) => {
428+
debug('starting mongos');
429+
return await MongoServer.start({
430+
...options,
431+
binary: 'mongos',
432+
args: [
433+
...(options.args ?? []),
434+
...args,
435+
'--configdb',
436+
`${configsvr.replSetName!}/${configsvr.hostport}`,
437+
],
438+
});
439+
}),
440+
);
441+
cluster.servers.push(...mongosServers);
442+
const mongos = mongosServers[0];
443+
await mongos.withClient(async (client) => {
444+
for (const shard of shardsvrs) {
445+
const shardSpec = `${shard.replSetName!}/${shard.hostport}`;
446+
debug('adding shard', shardSpec);
447+
await client.db('admin').command({
448+
addShard: shardSpec,
449+
});
450+
}
451+
debug('added shards');
452+
});
436453
}
437454

438455
await cluster.addAuthIfNeeded();
439456
return cluster;
440457
}
441458

442-
*children(): Iterable<MongoServer | MongoCluster> {
459+
private *children(): Iterable<MongoServer | MongoCluster> {
443460
yield* this.servers;
444461
yield* this.shards;
445462
}

packages/mongodb-runner/src/mongoserver.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ import type { Document, MongoClientOptions } from 'mongodb';
1313
import { MongoClient } from 'mongodb';
1414
import path from 'path';
1515
import { EventEmitter, once } from 'events';
16-
import { uuid, debug, pick, debugVerbose, jsonClone } from './util';
16+
import {
17+
uuid,
18+
debug,
19+
pick,
20+
debugVerbose,
21+
jsonClone,
22+
makeConnectionString,
23+
} from './util';
1724

1825
export interface MongoServerOptions {
1926
binDir?: string;
@@ -345,8 +352,17 @@ export class MongoServer extends EventEmitter<MongoServerEvents> {
345352
.collection<
346353
Omit<SerializedServerProperties, 'hasInsertedMetadataCollEntry'>
347354
>('mongodbrunner');
355+
// mongos hosts require a bit of special treatment because they do not have
356+
// local storage of their own, so we store the metadata in the config database,
357+
// which may be accessed by multiple mongos instances.
348358
debug('ensuring metadata collection entry', insertedInfo, { isMongoS });
349359
if (mode === 'insert-new') {
360+
const existingEntry = await runnerColl.findOne();
361+
if (!isMongoS && existingEntry) {
362+
throw new Error(
363+
`Unexpected mongodbrunner entry when creating new server: ${JSON.stringify(existingEntry)}`,
364+
);
365+
}
350366
await runnerColl.insertOne(insertedInfo);
351367
debug('inserted metadata collection entry', insertedInfo);
352368
this.hasInsertedMetadataCollEntry = true;
@@ -357,7 +373,9 @@ export class MongoServer extends EventEmitter<MongoServerEvents> {
357373
);
358374
return;
359375
}
360-
const match = await runnerColl.findOne();
376+
const match = await runnerColl.findOne(
377+
isMongoS ? { _id: this.uuid } : {},
378+
);
361379
debug('read metadata collection entry', insertedInfo, match);
362380
if (!match) {
363381
throw new Error(
@@ -397,11 +415,19 @@ export class MongoServer extends EventEmitter<MongoServerEvents> {
397415
return null;
398416
}
399417

418+
get connectionString(): string {
419+
return makeConnectionString(
420+
this.hostport,
421+
undefined,
422+
this.defaultConnectionOptions,
423+
);
424+
}
425+
400426
async withClient<Fn extends (client: MongoClient) => any>(
401427
fn: Fn,
402428
clientOptions: MongoClientOptions = {},
403429
): Promise<ReturnType<Fn>> {
404-
const client = await MongoClient.connect(`mongodb://${this.hostport}/`, {
430+
const client = await MongoClient.connect(this.connectionString, {
405431
directConnection: true,
406432
...this.defaultConnectionOptions,
407433
...clientOptions,

0 commit comments

Comments
 (0)