#413 Fix/nameChanges

Merged hasezoey hasezoey Pseudo commit used to compare (869d697...6279619)
Missing base report.

Unable to compare commits because the base of the pull request did not upload a coverage report.

Changes found in between 869d697...6279619 (pseudo...base) which prevent comparing this pull request.

Showing 9 of 70 files from the diff.
Other files ignored by Codecov
.releaserc.js has changed.
.eslintrc.js has changed.
tsconfig.json has changed.
package.json has changed.
CHANGELOG.md has changed.
README.md has changed.
yarn.lock has changed.

@@ -1,18 +1,30 @@
Loading
1 1
import { EventEmitter } from 'events';
2 -
import * as mongodb from 'mongodb';
3 -
import MongoMemoryServer from './MongoMemoryServer';
4 -
import { MongoMemoryServerOptsT } from './MongoMemoryServer';
5 -
import { assertion, generateDbName, getHost, isNullOrUndefined } from './util/db_util';
2 +
import MongoMemoryServer, { AutomaticAuth } from './MongoMemoryServer';
3 +
import { MongoMemoryServerOpts } from './MongoMemoryServer';
4 +
import {
5 +
  assertion,
6 +
  authDefault,
7 +
  ensureAsync,
8 +
  generateDbName,
9 +
  getHost,
10 +
  isNullOrUndefined,
11 +
} from './util/utils';
6 12
import { MongoBinaryOpts } from './util/MongoBinary';
7 -
import { MongoMemoryInstancePropT, MongoMemoryInstancePropBaseT, StorageEngineT } from './types';
8 13
import debug from 'debug';
9 -
import { MongoError } from 'mongodb';
10 -
import { deprecate } from 'util';
11 -
import { MongoInstanceEvents } from './util/MongoInstance';
14 +
import { MongoClient, MongoError } from 'mongodb';
15 +
import {
16 +
  MongoInstanceEvents,
17 +
  MongoMemoryInstanceProp,
18 +
  MongoMemoryInstancePropBase,
19 +
  StorageEngine,
20 +
} from './util/MongoInstance';
12 21
import { SpawnOptions } from 'child_process';
13 22
14 23
const log = debug('MongoMS:MongoMemoryReplSet');
15 24
25 +
// "setImmediate" is used to ensure the functions are async, otherwise the process might evaluate the one function before other async functions (like "start")
26 +
// and so skip to next state check or return before actually ready
27 +
16 28
/**
17 29
 * Replica set specific options.
18 30
 */
@@ -21,14 +33,15 @@
Loading
21 33
   * enable auth ("--auth" / "--noauth")
22 34
   * @default false
23 35
   */
24 -
  auth?: boolean;
36 +
  auth?: boolean | AutomaticAuth;
25 37
  /**
26 -
   * additional command line args passed to `mongod`
38 +
   * additional command line arguments passed to `mongod`
27 39
   * @default []
28 40
   */
29 41
  args?: string[];
30 42
  /**
31 -
   * number of `mongod` servers to start
43 +
   * if this number is bigger than "instanceOpts.length", more "generic" servers get started
44 +
   * if this number is lower than "instanceOpts.length", no more "generic" servers get started (server count will be "instanceOpts.length")
32 45
   * @default 1
33 46
   */
34 47
  count?: number;
@@ -47,11 +60,6 @@
Loading
47 60
   * @default 'testset'
48 61
   */
49 62
  name?: string;
50 -
  /**
51 -
   * oplog size (in MB)
52 -
   * @default 1
53 -
   */
54 -
  oplogSize?: number;
55 63
  /**
56 64
   * Childprocess spawn options
57 65
   * @default {}
@@ -61,18 +69,18 @@
Loading
61 69
   *`mongod` storage engine type
62 70
   * @default 'ephemeralForTest'
63 71
   */
64 -
  storageEngine?: StorageEngineT;
72 +
  storageEngine?: StorageEngine;
65 73
  /**
66 74
   * Options for "rsConfig"
67 75
   * @default {}
68 76
   */
69 -
  configSettings?: MongoMemoryReplSetConfigSettingsT;
77 +
  configSettings?: MongoMemoryReplSetConfigSettings;
70 78
}
71 79
72 80
/**
73 81
 * Options for "rsConfig"
74 82
 */
75 -
export interface MongoMemoryReplSetConfigSettingsT {
83 +
export interface MongoMemoryReplSetConfigSettings {
76 84
  chainingAllowed?: boolean;
77 85
  heartbeatTimeoutSecs?: number;
78 86
  heartbeatIntervalMillis?: number;
@@ -83,103 +91,181 @@
Loading
83 91
/**
84 92
 * Options for the replSet
85 93
 */
86 -
export interface MongoMemoryReplSetOptsT {
87 -
  instanceOpts?: MongoMemoryInstancePropBaseT[];
88 -
  binary?: MongoBinaryOpts;
89 -
  replSet?: ReplSetOpts;
94 +
export interface MongoMemoryReplSetOpts {
95 +
  /**
96 +
   * Specific Options to use for some instances
97 +
   */
98 +
  instanceOpts: MongoMemoryInstancePropBase[];
99 +
  /**
100 +
   * Binary Options used for all instances
101 +
   */
102 +
  binary: MongoBinaryOpts;
90 103
  /**
91 -
   * Auto-Start the replSet?
92 -
   * @default true
104 +
   * Options used for all instances
105 +
   * -> gets overwritten by specific "instanceOpts"
93 106
   */
94 -
  autoStart?: boolean;
107 +
  replSet: ReplSetOpts;
108 +
}
109 +
110 +
/**
111 +
 * Enum for "_state" inside "MongoMemoryReplSet"
112 +
 */
113 +
export enum MongoMemoryReplSetStates {
114 +
  init = 'init',
115 +
  running = 'running',
116 +
  stopped = 'stopped',
117 +
}
118 +
119 +
/**
120 +
 * All Events for "MongoMemoryReplSet"
121 +
 */
122 +
export enum MongoMemoryReplSetEvents {
123 +
  stateChange = 'stateChange',
124 +
}
125 +
126 +
export interface MongoMemoryReplSet extends EventEmitter {
127 +
  // Overwrite EventEmitter's definitions (to provide at least the event names)
128 +
  emit(event: MongoMemoryReplSetEvents, ...args: any[]): boolean;
129 +
  on(event: MongoMemoryReplSetEvents, listener: (...args: any[]) => void): this;
130 +
  once(event: MongoMemoryReplSetEvents, listener: (...args: any[]) => void): this;
95 131
}
96 132
97 133
/**
98 134
 * Class for managing an replSet
99 135
 */
100 136
export class MongoMemoryReplSet extends EventEmitter {
137 +
  /**
138 +
   * All servers this ReplSet instance manages
139 +
   */
101 140
  servers: MongoMemoryServer[] = [];
102 -
  opts: {
103 -
    instanceOpts: MongoMemoryInstancePropBaseT[];
104 -
    binary: MongoBinaryOpts;
105 -
    replSet: Required<ReplSetOpts>;
106 -
    autoStart?: boolean;
107 -
  };
108 141
109 -
  _state: 'init' | 'running' | 'stopped';
142 +
  // "!" is used, because the getters are used instead of the "_" values
143 +
  protected _instanceOpts!: MongoMemoryInstancePropBase[];
144 +
  protected _binaryOpts!: MongoBinaryOpts;
145 +
  protected _replSetOpts!: Required<ReplSetOpts>;
146 +
147 +
  protected _state: MongoMemoryReplSetStates = MongoMemoryReplSetStates.stopped;
148 +
  protected _ranCreateAuth: boolean = false;
110 149
111 -
  constructor(opts: MongoMemoryReplSetOptsT = {}) {
150 +
  constructor(opts: Partial<MongoMemoryReplSetOpts> = {}) {
112 151
    super();
113 -
    const replSetDefaults: Required<ReplSetOpts> = {
152 +
153 +
    this.binaryOpts = { ...opts.binary };
154 +
    this.instanceOpts = opts.instanceOpts ?? [];
155 +
    this.replSetOpts = { ...opts.replSet };
156 +
  }
157 +
158 +
  /**
159 +
   * Change "this._state" to "newState" and emit "newState"
160 +
   * @param newState The new State to set & emit
161 +
   */
162 +
  protected stateChange(newState: MongoMemoryReplSetStates, ...args: any[]): void {
163 +
    this._state = newState;
164 +
    this.emit(MongoMemoryReplSetEvents.stateChange, newState, ...args);
165 +
  }
166 +
167 +
  /**
168 +
   * Create an instance of "MongoMemoryReplSet" and call start
169 +
   * @param opts Options for the ReplSet
170 +
   */
171 +
  static async create(opts: Partial<MongoMemoryReplSetOpts> = {}): Promise<MongoMemoryReplSet> {
172 +
    const replSet = new this({ ...opts });
173 +
    await replSet.start();
174 +
    return replSet;
175 +
  }
176 +
177 +
  /**
178 +
   * Get Current state of this class
179 +
   */
180 +
  get state(): MongoMemoryReplSetStates {
181 +
    return this._state;
182 +
  }
183 +
184 +
  /**
185 +
   * Get & Set "instanceOpts"
186 +
   * @throws if "state" is not "stopped"
187 +
   */
188 +
  get instanceOpts(): MongoMemoryInstancePropBase[] {
189 +
    return this._instanceOpts;
190 +
  }
191 +
192 +
  set instanceOpts(val: MongoMemoryInstancePropBase[]) {
193 +
    assertion(
194 +
      this._state === MongoMemoryReplSetStates.stopped,
195 +
      new Error('Cannot change instance Options while "state" is not "stopped"!')
196 +
    );
197 +
    this._instanceOpts = val;
198 +
  }
199 +
200 +
  /**
201 +
   * Get & Set "binaryOpts"
202 +
   * @throws if "state" is not "stopped"
203 +
   */
204 +
  get binaryOpts(): MongoBinaryOpts {
205 +
    return this._binaryOpts;
206 +
  }
207 +
208 +
  set binaryOpts(val: MongoBinaryOpts) {
209 +
    assertion(
210 +
      this._state === MongoMemoryReplSetStates.stopped,
211 +
      new Error('Cannot change binary Options while "state" is not "stopped"!')
212 +
    );
213 +
    this._binaryOpts = val;
214 +
  }
215 +
216 +
  /**
217 +
   * Get & Set "replSetOpts"
218 +
   * (Applies defaults)
219 +
   * @throws if "state" is not "stopped"
220 +
   */
221 +
  get replSetOpts(): ReplSetOpts {
222 +
    return this._replSetOpts;
223 +
  }
224 +
225 +
  set replSetOpts(val: ReplSetOpts) {
226 +
    assertion(
227 +
      this._state === MongoMemoryReplSetStates.stopped,
228 +
      new Error('Cannot change replSet Options while "state" is not "stopped"!')
229 +
    );
230 +
    const defaults: Required<ReplSetOpts> = {
114 231
      auth: false,
115 232
      args: [],
116 233
      name: 'testset',
117 234
      count: 1,
118 235
      dbName: generateDbName(),
119 236
      ip: '127.0.0.1',
120 -
      oplogSize: 1,
121 237
      spawn: {},
122 238
      storageEngine: 'ephemeralForTest',
123 239
      configSettings: {},
124 240
    };
125 -
    this._state = 'stopped';
126 -
    this.opts = {
127 -
      binary: opts.binary || {},
128 -
      instanceOpts: opts.instanceOpts || [],
129 -
      replSet: { ...replSetDefaults, ...opts.replSet },
130 -
    };
241 +
    this._replSetOpts = { ...defaults, ...val };
131 242
132 -
    if (!this.opts.replSet.args) {
133 -
      this.opts.replSet.args = [];
134 -
    }
135 -
    this.opts.replSet.args.push('--oplogSize', `${this.opts.replSet.oplogSize}`);
136 -
    if (!(opts && opts.autoStart === false)) {
137 -
      log('Autostarting MongoMemoryReplSet.');
138 -
      setTimeout(() => this.start(), 0);
139 -
    }
140 -
141 -
    process.once('beforeExit', this.stop);
142 -
  }
143 -
144 -
  /**
145 -
   * Get the Connection String for mongodb to connect
146 -
   * @param otherDb use a different database than what was set on creation?
147 -
   * @deprecated
148 -
   */
149 -
  async getConnectionString(otherDb?: string | boolean): Promise<string> {
150 -
    return deprecate(
151 -
      this.getUri,
152 -
      '"MongoMemoryReplSet.getConnectionString" is deprecated, use ".getUri"',
153 -
      'MDEP001'
154 -
    ).call(this, otherDb);
155 -
  }
243 +
    assertion(this._replSetOpts.count > 0, new Error('ReplSet Count needs to be 1 or higher!'));
156 244
157 -
  /**
158 -
   * Returns database name.
159 -
   */
160 -
  async getDbName(): Promise<string> {
161 -
    // this function is only async for consistency with MongoMemoryServer
162 -
    // I don't see much point to either of them being async but don't
163 -
    // care enough to change it and introduce a breaking change.
164 -
    return this.opts.replSet.dbName;
245 +
    if (typeof this._replSetOpts.auth === 'object') {
246 +
      this._replSetOpts.auth = authDefault(this._replSetOpts.auth);
247 +
    }
165 248
  }
166 249
167 250
  /**
168 251
   * Returns instance options suitable for a MongoMemoryServer.
169 252
   * @param baseOpts Options to merge with
170 253
   */
171 -
  getInstanceOpts(baseOpts: MongoMemoryInstancePropBaseT = {}): MongoMemoryInstancePropT {
172 -
    const rsOpts: ReplSetOpts = this.opts.replSet;
173 -
    const opts: MongoMemoryInstancePropT = {
174 -
      auth: !!rsOpts.auth,
175 -
      args: rsOpts.args,
176 -
      dbName: rsOpts.dbName,
177 -
      ip: rsOpts.ip,
178 -
      replSet: rsOpts.name,
179 -
      storageEngine: rsOpts.storageEngine,
254 +
  protected getInstanceOpts(baseOpts: MongoMemoryInstancePropBase = {}): MongoMemoryInstanceProp {
255 +
    const opts: MongoMemoryInstanceProp = {
256 +
      // disable "auth" if replsetopts has an object-auth
257 +
      auth:
258 +
        typeof this._replSetOpts.auth === 'object' && !this._ranCreateAuth
259 +
          ? false
260 +
          : !!this._replSetOpts.auth,
261 +
      args: this._replSetOpts.args,
262 +
      dbName: this._replSetOpts.dbName,
263 +
      ip: this._replSetOpts.ip,
264 +
      replSet: this._replSetOpts.name,
265 +
      storageEngine: this._replSetOpts.storageEngine,
180 266
    };
181 267
    if (baseOpts.args) {
182 -
      opts.args = (rsOpts.args || []).concat(baseOpts.args);
268 +
      opts.args = (this._replSetOpts.args || []).concat(baseOpts.args);
183 269
    }
184 270
    if (baseOpts.port) {
185 271
      opts.port = baseOpts.port;
@@ -195,172 +281,270 @@
Loading
195 281
  }
196 282
197 283
  /**
198 -
   * Returns a mongodb: URI to connect to a given database.
284 +
   * Returns an mongodb URI that is setup with all replSet servers
199 285
   * @param otherDb use a different database than what was set on creation?
286 +
   * @throws if state is not "running"
287 +
   * @throws if an server doesnt have "instanceInfo.port" defined
200 288
   */
201 -
  async getUri(otherDb?: string | boolean): Promise<string> {
202 -
    if (this._state === 'init') {
203 -
      await this._waitForPrimary();
204 -
    }
205 -
    if (this._state !== 'running') {
206 -
      throw new Error('Replica Set is not running. Use debug for more info.');
207 -
    }
208 -
    let dbName: string;
209 -
    if (otherDb) {
210 -
      dbName = typeof otherDb === 'string' ? otherDb : generateDbName();
211 -
    } else {
212 -
      dbName = this.opts.replSet.dbName;
289 +
  getUri(otherDb?: string | boolean): string {
290 +
    log('getUri:', this._state);
291 +
    switch (this._state) {
292 +
      case MongoMemoryReplSetStates.running:
293 +
      case MongoMemoryReplSetStates.init:
294 +
        break;
295 +
      case MongoMemoryReplSetStates.stopped:
296 +
      default:
297 +
        throw new Error('Replica Set is not running. Use debug for more info.');
213 298
    }
299 +
300 +
    const dbName: string = isNullOrUndefined(otherDb)
301 +
      ? this._replSetOpts.dbName
302 +
      : typeof otherDb === 'string'
303 +
      ? otherDb
304 +
      : generateDbName();
214 305
    const ports = this.servers.map((s) => {
215 306
      const port = s.instanceInfo?.port;
216 307
      assertion(!isNullOrUndefined(port), new Error('Instance Port is undefined!'));
217 308
      return port;
218 309
    });
219 310
    const hosts = ports.map((port) => `127.0.0.1:${port}`).join(',');
220 -
    return `mongodb://${hosts}/${dbName}?replicaSet=${this.opts.replSet.name}`;
311 +
    return `mongodb://${hosts}/${dbName}?replicaSet=${this._replSetOpts.name}`;
221 312
  }
222 313
223 314
  /**
224 315
   * Start underlying `mongod` instances.
316 +
   * @throws if state is already "running"
225 317
   */
226 318
  async start(): Promise<void> {
227 319
    log('start');
228 -
    if (this._state !== 'stopped') {
229 -
      throw new Error(`Already in 'init' or 'running' state. Use debug for more info.`);
320 +
    switch (this._state) {
321 +
      // case MongoMemoryReplSetStateEnum.init:
322 +
      //   return this.waitUntilRunning();
323 +
      case MongoMemoryReplSetStates.stopped:
324 +
        break;
325 +
      case MongoMemoryReplSetStates.running:
326 +
      default:
327 +
        throw new Error('Already in "init" or "running" state. Use debug for more info.');
230 328
    }
231 -
    this.emit((this._state = 'init'));
329 +
    this.stateChange(MongoMemoryReplSetStates.init); // this needs to be executed before "setImmediate"
330 +
    await ensureAsync();
232 331
    log('init');
233 -
    // Any servers defined within `opts.instanceOpts` should be started first as
332 +
    await this.initAllServers();
333 +
    await this._initReplSet();
334 +
    process.once('beforeExit', this.stop);
335 +
  }
336 +
337 +
  protected async initAllServers(): Promise<void> {
338 +
    this.stateChange(MongoMemoryReplSetStates.init);
339 +
340 +
    if (this.servers.length > 0) {
341 +
      log('initAllServers: lenght of "servers" is higher than 0, starting existing servers');
342 +
      await Promise.all(this.servers.map((s) => s.start(true)));
343 +
344 +
      return;
345 +
    }
346 +
347 +
    // Any servers defined within `_instanceOpts` should be started first as
234 348
    // the user could have specified a `dbPath` in which case we would want to perform
235 349
    // the `replSetInitiate` command against that server.
236 -
    const servers = this.opts.instanceOpts.map((opts) => {
237 -
      log('  starting server from instanceOpts:', opts, '...');
238 -
      return this._initServer(this.getInstanceOpts(opts));
350 +
    this._instanceOpts.forEach((opts) => {
351 +
      log(`  starting server from instanceOpts (count: ${this.servers.length + 1}):`, opts);
352 +
      this.servers.push(this._initServer(this.getInstanceOpts(opts)));
239 353
    });
240 -
    const cnt = this.opts.replSet.count || 1;
241 -
    while (servers.length < cnt) {
242 -
      log(`  starting server ${servers.length + 1} of ${cnt}...`);
243 -
      const server = this._initServer(this.getInstanceOpts({}));
244 -
      servers.push(server);
354 +
    while (this.servers.length < this._replSetOpts.count) {
355 +
      log(`  starting server ${this.servers.length + 1} of ${this._replSetOpts.count}`);
356 +
      this.servers.push(this._initServer(this.getInstanceOpts()));
245 357
    }
358 +
246 359
    // ensures all servers are listening for connection
247 -
    await Promise.all(servers.map((s) => s.start()));
248 -
    this.servers = servers;
249 -
    await this._initReplSet();
360 +
    await Promise.all(this.servers.map((s) => s.start()));
250 361
  }
251 362
252 363
  /**
253 364
   * Stop the underlying `mongod` instance(s).
254 365
   */
255 366
  async stop(): Promise<boolean> {
256 -
    if (this._state === 'stopped') {
367 +
    log('stop' + isNullOrUndefined(process.exitCode) ? '' : ': called by process-event');
368 +
    process.removeListener('beforeExit', this.stop); // many accumulate inside tests
369 +
    if (this._state === MongoMemoryReplSetStates.stopped) {
257 370
      return false;
258 371
    }
259 -
    process.removeListener('beforeExit', this.stop); // many accumulate inside tests
260 -
    return Promise.all(this.servers.map((s) => s.stop()))
372 +
    return Promise.all(this.servers.map((s) => s.stop(false)))
261 373
      .then(() => {
262 -
        this.servers = [];
263 -
        this.emit((this._state = 'stopped'));
374 +
        this.stateChange(MongoMemoryReplSetStates.stopped);
264 375
        return true;
265 376
      })
266 377
      .catch((err) => {
267 -
        this.servers = [];
268 378
        log(err);
269 -
        this.emit((this._state = 'stopped'), err);
379 +
        this.stateChange(MongoMemoryReplSetStates.stopped, err);
270 380
        return false;
271 381
      });
272 382
  }
273 383
384 +
  /**
385 +
   * Remove the defined dbPath's
386 +
   * This function gets automatically called on process event "beforeExit" (with force being "false")
387 +
   * @param force Remove the dbPath even if it is no "tmpDir" (and re-check if tmpDir actually removed it)
388 +
   * @throws If "state" is not "stopped"
389 +
   * @throws If "instanceInfo" is not defined
390 +
   * @throws If an fs error occured
391 +
   */
392 +
  async cleanup(force: boolean = false): Promise<void> {
393 +
    assertion(
394 +
      this._state === MongoMemoryReplSetStates.stopped,
395 +
      new Error('Cannot run cleanup when state is not "stopped"')
396 +
    );
397 +
    log(`cleanup for "${this.servers.length}" servers`);
398 +
    await Promise.all(this.servers.map((s) => s.cleanup(force)));
399 +
400 +
    this.servers = [];
401 +
402 +
    return;
403 +
  }
404 +
274 405
  /**
275 406
   * Wait until all instances are running
407 +
   * @throws if state is "stopped" (cannot wait on something that dosnt start)
276 408
   */
277 409
  async waitUntilRunning(): Promise<void> {
278 410
    // TODO: this seems like it dosnt catch if an instance fails, and runs forever
279 -
    if (this._state === 'running') {
280 -
      return;
411 +
    await ensureAsync();
412 +
    log('waitUntilRunning:', this._state);
413 +
    switch (this._state) {
414 +
      case MongoMemoryReplSetStates.running:
415 +
        // just return immediatly if the replSet is already running
416 +
        return;
417 +
      case MongoMemoryReplSetStates.init:
418 +
        // wait for event "running"
419 +
        await new Promise((res) => {
420 +
          // the use of "this" here can be done because "on" either binds "this" or uses an arrow function
421 +
          function waitRunning(this: MongoMemoryReplSet, state: MongoMemoryReplSetStates) {
422 +
            // this is because other states can be emitted multiple times (like stopped & init for auth creation)
423 +
            if (state === MongoMemoryReplSetStates.running) {
424 +
              this.removeListener(MongoMemoryReplSetEvents.stateChange, waitRunning);
425 +
              res();
426 +
            }
427 +
          }
428 +
          this.on(MongoMemoryReplSetEvents.stateChange, waitRunning);
429 +
        });
430 +
        return;
431 +
      case MongoMemoryReplSetStates.stopped:
432 +
      default:
433 +
        // throw an error if not "running" or "init"
434 +
        throw new Error(
435 +
          'State is not "running" or "init" - cannot wait on something that dosnt start'
436 +
        );
281 437
    }
282 -
    await new Promise((resolve) => this.once('running', () => resolve()));
283 438
  }
284 439
285 440
  /**
286 441
   * Connects to the first server from the list of servers and issues the `replSetInitiate`
287 442
   * command passing in a new replica set configuration object.
443 +
   * @throws if state is not "init"
444 +
   * @throws if "servers.length" is not 1 or above
445 +
   * @throws if package "mongodb" is not installed
288 446
   */
289 -
  async _initReplSet(): Promise<void> {
290 -
    if (this._state !== 'init') {
291 -
      throw new Error('Not in init phase.');
292 -
    }
293 -
    log('Initializing replica set.');
294 -
    if (!this.servers.length) {
295 -
      throw new Error('One or more servers are required.');
296 -
    }
447 +
  protected async _initReplSet(): Promise<void> {
448 +
    log('_initReplSet');
449 +
    assertion(this._state === MongoMemoryReplSetStates.init, new Error('Not in init phase.'));
450 +
    assertion(this.servers.length > 0, new Error('One or more servers are required.'));
297 451
    const uris = this.servers.map((server) => server.getUri());
298 452
299 -
    let MongoClient: typeof mongodb.MongoClient;
300 -
    try {
301 -
      MongoClient = (await import('mongodb')).MongoClient;
302 -
    } catch (e) {
303 -
      throw new Error(
304 -
        `You need to install "mongodb" package. It's required for checking ReplicaSet state.`
305 -
      );
306 -
    }
307 -
308 -
    const conn: mongodb.MongoClient = await MongoClient.connect(uris[0], {
453 +
    let con: MongoClient = await MongoClient.connect(uris[0], {
309 454
      useNewUrlParser: true,
310 455
      useUnifiedTopology: true,
311 456
    });
312 457
313 458
    try {
314 -
      const db = await conn.db(this.opts.replSet.dbName);
459 +
      let adminDb = con.db('admin');
315 460
316 -
      // MongoClient HACK which helps to avoid the following error:
317 -
      //   "RangeError: Maximum call stack size exceeded"
318 -
      // (db as any).topology.shouldCheckForSessionSupport = () => false; // TODO: remove after 1.1.2021 if no issues arise
319 -
320 -
      /** reference to "db.admin()" */
321 -
      const admin = db.admin();
322 461
      const members = uris.map((uri, idx) => ({ _id: idx, host: getHost(uri) }));
323 462
      const rsConfig = {
324 -
        _id: this.opts.replSet.name,
463 +
        _id: this._replSetOpts.name,
325 464
        members,
326 465
        settings: {
327 466
          electionTimeoutMillis: 500,
328 -
          ...(this.opts.replSet.configSettings || {}),
467 +
          ...this._replSetOpts.configSettings,
329 468
        },
330 469
      };
331 470
      try {
332 -
        await admin.command({ replSetInitiate: rsConfig });
471 +
        await adminDb.command({ replSetInitiate: rsConfig });
472 +
473 +
        if (typeof this._replSetOpts.auth === 'object') {
474 +
          log('_initReplSet: "this._replSetOpts.auth" is an object');
475 +
476 +
          await this._waitForPrimary();
477 +
478 +
          const primary = this.servers.find(
479 +
            (server) => server.instanceInfo?.instance.isInstancePrimary
480 +
          );
481 +
          assertion(!isNullOrUndefined(primary), new Error('No Primary found'));
482 +
          assertion(
483 +
            !isNullOrUndefined(primary.instanceInfo),
484 +
            new Error('Primary dosnt have an "instanceInfo" defined')
485 +
          );
486 +
487 +
          await primary.createAuth(primary.instanceInfo);
488 +
          this._ranCreateAuth = true;
489 +
490 +
          if (primary.opts.instance?.storageEngine !== 'ephemeralForTest') {
491 +
            log('_initReplSet: closing connection for restart');
492 +
            await con.close(); // close connection in preparation for "stop"
493 +
            await this.stop(); // stop all servers for enabling auth
494 +
            log('_initReplSet: starting all server again with auth');
495 +
            await this.initAllServers(); // start all servers again with "auth" enabled
496 +
497 +
            con = await MongoClient.connect(this.getUri('admin'), {
498 +
              useNewUrlParser: true,
499 +
              useUnifiedTopology: true,
500 +
              authSource: 'admin',
501 +
              authMechanism: 'SCRAM-SHA-256',
502 +
              auth: {
503 +
                user: this._replSetOpts.auth.customRootName as string, // cast because these are existing
504 +
                password: this._replSetOpts.auth.customRootPwd as string,
505 +
              },
506 +
            });
507 +
            adminDb = con.db('admin');
508 +
            log('_initReplSet: auth restart finished');
509 +
          } else {
510 +
            console.warn(
511 +
              'Not Restarting ReplSet for Auth\n' +
512 +
                'Storage engine of current PRIMARY is ephemeralForTest, which does not write data on shutdown, and mongodb does not allow changeing "auth" runtime'
513 +
            );
514 +
          }
515 +
        }
333 516
      } catch (e) {
334 517
        if (e instanceof MongoError && e.errmsg == 'already initialized') {
335 518
          log(`${e.errmsg}: trying to set old config`);
336 -
          const { config: oldConfig } = await admin.command({ replSetGetConfig: 1 });
519 +
          const { config: oldConfig } = await adminDb.command({ replSetGetConfig: 1 });
337 520
          log('got old config:\n', oldConfig);
338 -
          await admin.command({
521 +
          await adminDb.command({
339 522
            replSetReconfig: oldConfig,
340 523
            force: true,
341 524
          });
342 525
        } else {
343 526
          throw e;
344 527
        }
345 528
      }
346 -
      log('Waiting for replica set to have a PRIMARY member.');
529 +
      log('_initReplSet: ReplSet-reConfig finished');
347 530
      await this._waitForPrimary();
348 -
      this.emit((this._state = 'running'));
349 -
      log('running');
531 +
      this.stateChange(MongoMemoryReplSetStates.running);
532 +
      log('_initReplSet: running');
350 533
    } finally {
351 -
      await conn.close();
534 +
      await con.close();
352 535
    }
353 536
  }
354 537
355 538
  /**
356 539
   * Create the one Instance (without starting them)
357 540
   * @param instanceOpts Instance Options to use for this instance
358 541
   */
359 -
  _initServer(instanceOpts: MongoMemoryInstancePropT): MongoMemoryServer {
360 -
    const serverOpts: MongoMemoryServerOptsT = {
361 -
      binary: this.opts.binary,
542 +
  protected _initServer(instanceOpts: MongoMemoryInstanceProp): MongoMemoryServer {
543 +
    const serverOpts: MongoMemoryServerOpts = {
544 +
      binary: this._binaryOpts,
362 545
      instance: instanceOpts,
363 -
      spawn: this.opts.replSet.spawn,
546 +
      spawn: this._replSetOpts.spawn,
547 +
      auth: typeof this.replSetOpts.auth === 'object' ? this.replSetOpts.auth : undefined,
364 548
    };
365 549
    const server = new MongoMemoryServer(serverOpts);
366 550
    return server;
@@ -369,14 +553,10 @@
Loading
369 553
  /**
370 554
   * Wait until the replSet has elected an Primary
371 555
   * @param timeout Timeout to not run infinitly
556 +
   * @throws if timeout is reached
372 557
   */
373 -
  async _waitForPrimary(timeout: number = 30000): Promise<void> {
558 +
  protected async _waitForPrimary(timeout: number = 30000): Promise<void> {
374 559
    let timeoutId: NodeJS.Timeout | undefined;
375 -
    const timeoutPromise = new Promise((resolve, reject) => {
376 -
      timeoutId = setTimeout(() => {
377 -
        reject('Timed out in ' + timeout + 'ms. When waiting for primary.');
378 -
      }, timeout);
379 -
    });
380 560
381 561
    await Promise.race([
382 562
      ...this.servers.map(
@@ -387,12 +567,21 @@
Loading
387 567
              return rej(new Error('_waitForPrimary - instanceInfo not present '));
388 568
            }
389 569
            instanceInfo.instance.once(MongoInstanceEvents.instancePrimary, res);
570 +
571 +
            if (instanceInfo.instance.isInstancePrimary) {
572 +
              log('_waitForPrimary: found instance being already primary');
573 +
              res();
574 +
            }
390 575
          })
391 576
      ),
392 -
      timeoutPromise,
577 +
      new Promise((res, rej) => {
578 +
        timeoutId = setTimeout(() => {
579 +
          rej(new Error(`Timed out after ${timeout}ms while waiting for an Primary`));
580 +
        }, timeout);
581 +
      }),
393 582
    ]);
394 583
395 -
    if (timeoutId != null) {
584 +
    if (!isNullOrUndefined(timeoutId)) {
396 585
      clearTimeout(timeoutId);
397 586
    }
398 587

@@ -1,12 +1,29 @@
Loading
1 1
import { SpawnOptions } from 'child_process';
2 2
import * as tmp from 'tmp';
3 3
import getPort from 'get-port';
4 -
import { assertion, generateDbName, getUriBase, isNullOrUndefined } from './util/db_util';
5 -
import MongoInstance from './util/MongoInstance';
4 +
import {
5 +
  assertion,
6 +
  generateDbName,
7 +
  uriTemplate,
8 +
  isNullOrUndefined,
9 +
  authDefault,
10 +
  statPath,
11 +
} from './util/utils';
12 +
import MongoInstance, {
13 +
  MongodOpts,
14 +
  MongoMemoryInstanceProp,
15 +
  StorageEngine,
16 +
} from './util/MongoInstance';
6 17
import { MongoBinaryOpts } from './util/MongoBinary';
7 -
import { MongoMemoryInstancePropT, StorageEngineT } from './types';
8 18
import debug from 'debug';
9 19
import { EventEmitter } from 'events';
20 +
import { promises as fspromises } from 'fs';
21 +
import { MongoClient } from 'mongodb';
22 +
import { lt } from 'semver';
23 +
24 +
// this is because "import {promises: {readdir}}" is not valid syntax
25 +
// const { readdir, stat, rmdir } = promises;
26 +
// the statement above cannot be done, because otherwise in the tests no spy / mock can be applied
10 27
11 28
const log = debug('MongoMS:MongoMemoryServer');
12 29
@@ -15,10 +32,42 @@
Loading
15 32
/**
16 33
 * MongoMemoryServer Stored Options
17 34
 */
18 -
export interface MongoMemoryServerOptsT {
19 -
  instance?: MongoMemoryInstancePropT;
35 +
export interface MongoMemoryServerOpts {
36 +
  instance?: MongoMemoryInstanceProp;
20 37
  binary?: MongoBinaryOpts;
21 38
  spawn?: SpawnOptions;
39 +
  /**
40 +
   * Defining this enables automatic user creation
41 +
   */
42 +
  auth?: AutomaticAuth;
43 +
}
44 +
45 +
export interface AutomaticAuth {
46 +
  /**
47 +
   * Disable Automatic User creation
48 +
   * @default false because when defining this object it usually means that AutomaticAuth is wanted
49 +
   */
50 +
  disable?: boolean;
51 +
  /**
52 +
   * Extra Users to create besides the root user
53 +
   * @default []
54 +
   */
55 +
  extraUsers?: CreateUser[];
56 +
  /**
57 +
   * mongodb-memory-server automatically creates an root user (with "root" role)
58 +
   * @default 'mongodb-memory-server-root'
59 +
   */
60 +
  customRootName?: string;
61 +
  /**
62 +
   * mongodb-memory-server automatically creates an root user with this password
63 +
   * @default 'rootuser'
64 +
   */
65 +
  customRootPwd?: string;
66 +
  /**
67 +
   * Force to run "createAuth"
68 +
   * @default false "creatAuth" is normally only run when the given "dbPath" is empty (no files)
69 +
   */
70 +
  force?: boolean;
22 71
}
23 72
24 73
/**
@@ -29,64 +78,158 @@
Loading
29 78
  dbPath?: string;
30 79
  dbName: string;
31 80
  ip: string;
32 -
  storageEngine: StorageEngineT;
81 +
  storageEngine: StorageEngine;
33 82
  replSet?: string;
34 83
  tmpDir?: tmp.DirResult;
35 84
}
36 85
37 86
/**
38 87
 * Information about the currently running instance
39 88
 */
40 -
export interface MongoInstanceDataT extends StartupInstanceData {
89 +
export interface MongoInstanceData extends StartupInstanceData {
41 90
  dbPath: string; // re-declare, because in this interface it is *not* optional
42 91
  instance: MongoInstance;
43 92
}
44 93
45 94
/**
46 95
 * All Events for "MongoMemoryServer"
47 96
 */
48 -
export enum MongoMemoryServerEventEnum {
97 +
export enum MongoMemoryServerEvents {
49 98
  stateChange = 'stateChange',
50 99
}
51 100
52 101
/**
53 102
 * All States for "MongoMemoryServer._state"
54 103
 */
55 -
export enum MongoMemoryServerStateEnum {
104 +
export enum MongoMemoryServerStates {
56 105
  new = 'new',
57 106
  starting = 'starting',
58 107
  running = 'running',
59 108
  stopped = 'stopped',
60 109
}
61 110
111 +
/**
112 +
 * All MongoDB Built-in Roles
113 +
 * @see https://docs.mongodb.com/manual/reference/built-in-roles/
114 +
 */
115 +
export type UserRoles =
116 +
  | 'read'
117 +
  | 'readWrite'
118 +
  | 'dbAdmin'
119 +
  | 'dbOwner'
120 +
  | 'userAdmin'
121 +
  | 'clusterAdmin'
122 +
  | 'clusterManager'
123 +
  | 'clusterMonitor'
124 +
  | 'hostManager'
125 +
  | 'backup'
126 +
  | 'restore'
127 +
  | 'readAnyDatabase'
128 +
  | 'readWriteAnyDatabase'
129 +
  | 'userAdminAnyDatabase'
130 +
  | 'dbAdminAnyDatabase'
131 +
  | 'root'
132 +
  | string;
133 +
134 +
/**
135 +
 * Interface options for "db.createUser" (used for this package)
136 +
 * This interface is WITHOUT the custom options from this package
137 +
 * (Some text copied from https://docs.mongodb.com/manual/reference/method/db.createUser/#definition)
138 +
 */
139 +
export interface CreateUserMongoDB {
140 +
  /**
141 +
   * Username
142 +
   */
143 +
  createUser: string;
144 +
  /**
145 +
   * Password
146 +
   */
147 +
  pwd: string;
148 +
  /**
149 +
   * Any arbitrary information.
150 +
   * This field can be used to store any data an admin wishes to associate with this particular user.
151 +
   * @example this could be the user’s full name or employee id.
152 +
   */
153 +
  customData?: {
154 +
    [key: string]: any;
155 +
  };
156 +
  /**
157 +
   * The Roles for the user, can be an empty array
158 +
   */
159 +
  roles: ({ role: UserRoles; db: string } | UserRoles)[];
160 +
  /**
161 +
   * Specify the specific SCRAM mechanism or mechanisms for creating SCRAM user credentials.
162 +
   */
163 +
  mechanisms?: ('SCRAM-SHA-1' | 'SCRAM-SHA-256')[];
164 +
  /**
165 +
   * The authentication restrictions the server enforces on the created user.
166 +
   * Specifies a list of IP addresses and CIDR ranges from which the user is allowed to connect to the server or from which the server can accept users.
167 +
   */
168 +
  authenticationRestrictions?: {
169 +
    clientSource?: string;
170 +
    serverAddress?: string;
171 +
  }[];
172 +
  /**
173 +
   * Indicates whether the server or the client digests the password.
174 +
   * "true" - The Server digests the Password
175 +
   * "false" - The Client digests the Password
176 +
   */
177 +
  digestPassword?: boolean;
178 +
}
179 +
180 +
/**
181 +
 * Interface options for "db.createUser" (used for this package)
182 +
 * This interface is WITH the custom options from this package
183 +
 * (Some text copied from https://docs.mongodb.com/manual/reference/method/db.createUser/#definition)
184 +
 */
185 +
export interface CreateUser extends CreateUserMongoDB {
186 +
  /**
187 +
   * In which Database to create this user in
188 +
   * @default 'admin' by default the "admin" database is used
189 +
   */
190 +
  database?: string;
191 +
}
192 +
193 +
export interface MongoMemoryServerGetStartOptions {
194 +
  createAuth: boolean;
195 +
  data: StartupInstanceData;
196 +
  mongodOptions: Partial<MongodOpts>;
197 +
}
198 +
62 199
export interface MongoMemoryServer extends EventEmitter {
63 200
  // Overwrite EventEmitter's definitions (to provide at least the event names)
64 -
  emit(event: MongoMemoryServerEventEnum, ...args: any[]): boolean;
65 -
  on(event: MongoMemoryServerEventEnum, listener: (...args: any[]) => void): this;
66 -
  once(event: MongoMemoryServerEventEnum, listener: (...args: any[]) => void): this;
201 +
  emit(event: MongoMemoryServerEvents, ...args: any[]): boolean;
202 +
  on(event: MongoMemoryServerEvents, listener: (...args: any[]) => void): this;
203 +
  once(event: MongoMemoryServerEvents, listener: (...args: any[]) => void): this;
67 204
}
68 205
69 206
export class MongoMemoryServer extends EventEmitter {
70 -
  protected _instanceInfo?: MongoInstanceDataT;
71 -
  opts: MongoMemoryServerOptsT;
72 -
  protected _state: MongoMemoryServerStateEnum = MongoMemoryServerStateEnum.new;
207 +
  protected _instanceInfo?: MongoInstanceData;
208 +
  opts: MongoMemoryServerOpts;
209 +
  protected _state: MongoMemoryServerStates = MongoMemoryServerStates.new;
210 +
  readonly auth?: Required<AutomaticAuth>;
73 211
74 212
  /**
75 213
   * Create an Mongo-Memory-Sever Instance
76 214
   *
77 215
   * Note: because of JavaScript limitations, autoStart cannot be awaited here, use ".create" for async/await ability
78 216
   * @param opts Mongo-Memory-Sever Options
79 217
   */
80 -
  constructor(opts?: MongoMemoryServerOptsT) {
218 +
  constructor(opts?: MongoMemoryServerOpts) {
81 219
    super();
82 220
    this.opts = { ...opts };
221 +
222 +
    if (!isNullOrUndefined(this.opts.auth)) {
223 +
      // assign defaults
224 +
      this.auth = authDefault(this.opts.auth);
225 +
    }
83 226
  }
84 227
85 228
  /**
86 229
   * Create an Mongo-Memory-Sever Instance that can be awaited
87 230
   * @param opts Mongo-Memory-Sever Options
88 231
   */
89 -
  static async create(opts?: MongoMemoryServerOptsT): Promise<MongoMemoryServer> {
232 +
  static async create(opts?: MongoMemoryServerOpts): Promise<MongoMemoryServer> {
90 233
    log('Called MongoMemoryServer.create() method');
91 234
    const instance = new MongoMemoryServer({ ...opts });
92 235
    await instance.start();
@@ -98,46 +241,84 @@
Loading
98 241
   * Change "this._state" to "newState" and emit "stateChange" with "newState"
99 242
   * @param newState The new State to set & emit
100 243
   */
101 -
  protected stateChange(newState: MongoMemoryServerStateEnum): void {
244 +
  protected stateChange(newState: MongoMemoryServerStates): void {
102 245
    this._state = newState;
103 -
    this.emit(MongoMemoryServerEventEnum.stateChange, newState);
246 +
    this.emit(MongoMemoryServerEvents.stateChange, newState);
104 247
  }
105 248
106 249
  /**
107 250
   * Start the in-memory Instance
251 +
   * @param forceSamePort Force to use the Same Port, if already an "instanceInfo" exists
108 252
   */
109 -
  async start(): Promise<boolean> {
253 +
  async start(forceSamePort: boolean = false): Promise<boolean> {
110 254
    log('Called MongoMemoryServer.start() method');
111 -
    if (this._instanceInfo) {
112 -
      throw new Error(
113 -
        'MongoDB instance already in status startup/running/error. Use debug for more info.'
114 -
      );
255 +
256 +
    switch (this._state) {
257 +
      case MongoMemoryServerStates.new:
258 +
      case MongoMemoryServerStates.stopped:
259 +
        break;
260 +
      case MongoMemoryServerStates.running:
261 +
      case MongoMemoryServerStates.starting:
262 +
      default:
263 +
        throw new Error('Already in state running/starting or unkown');
115 264
    }
116 265
117 -
    this.stateChange(MongoMemoryServerStateEnum.starting);
266 +
    if (!isNullOrUndefined(this._instanceInfo?.instance.childProcess)) {
267 +
      throw new Error('Cannot start because "instance.childProcess" is already defined!');
268 +
    }
269 +
270 +
    this.stateChange(MongoMemoryServerStates.starting);
271 +
272 +
    // check if an "beforeExit" listener for "this.cleanup" is already defined for this class, if not add one
273 +
    if (
274 +
      process
275 +
        .listeners('beforeExit')
276 +
        .findIndex((f: (...args: any[]) => any) => f === this.cleanup) <= -1
277 +
    ) {
278 +
      process.on('beforeExit', this.cleanup);
279 +
    }
118 280
119 -
    this._instanceInfo = await this._startUpInstance().catch((err) => {
281 +
    await this._startUpInstance(forceSamePort).catch((err) => {
120 282
      if (!debug.enabled('MongoMS:MongoMemoryServer')) {
121 283
        console.warn('Starting the instance failed, enable debug for more infomation');
122 284
      }
123 285
      throw err;
124 286
    });
125 287
126 -
    this.stateChange(MongoMemoryServerStateEnum.running);
288 +
    this.stateChange(MongoMemoryServerStates.running);
127 289
128 290
    return true;
129 291
  }
130 292
131 293
  /**
132 -
   * Internal Function to start an instance
133 -
   * @private
294 +
   * Find an new unlocked port
295 +
   * @param port An User defined default port
134 296
   */
135 -
  async _startUpInstance(): Promise<MongoInstanceDataT> {
136 -
    log('Called MongoMemoryServer._startUpInstance() method');
297 +
  protected async getNewPort(port?: number): Promise<number> {
298 +
    const newPort = await getPort({ port });
299 +
300 +
    // only log this message if an custom port was provided
301 +
    if (port != newPort && typeof port === 'number') {
302 +
      log(`starting with port ${newPort}, since ${port} was locked`);
303 +
    }
304 +
305 +
    return newPort;
306 +
  }
307 +
308 +
  /**
309 +
   * Construct Instance Starting Options
310 +
   */
311 +
  protected async getStartOptions(): Promise<MongoMemoryServerGetStartOptions> {
312 +
    log('getStartOptions');
137 313
    /** Shortcut to this.opts.instance */
138 314
    const instOpts = this.opts.instance ?? {};
315 +
    /**
316 +
     * This variable is used for determining if "createAuth" should be run
317 +
     */
318 +
    let isNew: boolean = true;
319 +
139 320
    const data: StartupInstanceData = {
140 -
      port: await getPort({ port: instOpts.port ?? undefined }), // do (null or undefined) to undefined
321 +
      port: await this.getNewPort(instOpts.port ?? undefined), // do (null or undefined) to undefined
141 322
      dbName: generateDbName(instOpts.dbName),
142 323
      ip: instOpts.ip ?? '127.0.0.1',
143 324
      storageEngine: instOpts.storageEngine ?? 'ephemeralForTest',
@@ -146,52 +327,132 @@
Loading
146 327
      tmpDir: undefined,
147 328
    };
148 329
149 -
    if (instOpts.port != data.port) {
150 -
      log(`starting with port ${data.port}, since ${instOpts.port} was locked:`, data.port);
330 +
    if (isNullOrUndefined(this._instanceInfo)) {
331 +
      // create an tmpDir instance if no "dbPath" is given
332 +
      if (!data.dbPath) {
333 +
        data.tmpDir = tmp.dirSync({
334 +
          mode: 0o755,
335 +
          prefix: 'mongo-mem-',
336 +
          unsafeCleanup: true,
337 +
        });
338 +
        data.dbPath = data.tmpDir.name;
339 +
340 +
        isNew = true; // just to ensure "isNew" is "true" because an new temporary directory got created
341 +
      } else {
342 +
        log(`Checking if "${data.dbPath}}" (no new tmpDir) already has data`);
343 +
        const files = await fspromises.readdir(data.dbPath);
344 +
345 +
        isNew = files.length > 0; // if there already files in the directory, assume that the database is not new
346 +
      }
347 +
    } else {
348 +
      isNew = false;
151 349
    }
152 350
153 -
    if (!data.dbPath) {
154 -
      data.tmpDir = tmp.dirSync({
155 -
        mode: 0o755,
156 -
        prefix: 'mongo-mem-',
157 -
        unsafeCleanup: true,
158 -
      });
159 -
      data.dbPath = data.tmpDir.name;
351 +
    const createAuth: boolean =
352 +
      !!instOpts.auth && // check if auth is even meant to be enabled
353 +
      !isNullOrUndefined(this.auth) && // check if "this.auth" is defined
354 +
      !this.auth.disable && // check that "this.auth.disable" is falsey
355 +
      (this.auth.force || isNew) && // check that either "isNew" or "this.auth.force" is "true"
356 +
      !instOpts.replSet; // dont run "createAuth" when its an replset
357 +
358 +
    return {
359 +
      data: data,
360 +
      createAuth: createAuth,
361 +
      mongodOptions: {
362 +
        instance: {
363 +
          dbPath: data.dbPath,
364 +
          ip: data.ip,
365 +
          port: data.port,
366 +
          storageEngine: data.storageEngine,
367 +
          replSet: data.replSet,
368 +
          args: instOpts.args,
369 +
          auth: createAuth ? false : instOpts.auth, // disable "auth" for "createAuth"
370 +
        },
371 +
        binary: this.opts.binary,
372 +
        spawn: this.opts.spawn,
373 +
      },
374 +
    };
375 +
  }
376 +
377 +
  /**
378 +
   * Internal Function to start an instance
379 +
   * @param forceSamePort Force to use the Same Port, if already an "instanceInfo" exists
380 +
   * @private
381 +
   */
382 +
  async _startUpInstance(forceSamePort: boolean = false): Promise<void> {
383 +
    log('Called MongoMemoryServer._startUpInstance() method');
384 +
385 +
    if (!isNullOrUndefined(this._instanceInfo)) {
386 +
      log('_startUpInstance: "instanceInfo" already defined, reusing instance');
387 +
      if (!forceSamePort) {
388 +
        const newPort = await this.getNewPort(this._instanceInfo.port);
389 +
        this._instanceInfo.instance.instanceOpts.port = newPort;
390 +
        this._instanceInfo.port = newPort;
391 +
      }
392 +
      await this._instanceInfo.instance.run();
393 +
394 +
      return;
160 395
    }
161 396
162 -
    log(`Starting MongoDB instance with options: ${JSON.stringify(data)}`);
397 +
    const { mongodOptions, createAuth, data } = await this.getStartOptions();
398 +
    log(`Creating new MongoDB instance with options: ${JSON.stringify(mongodOptions)}`);
163 399
164 400
    // After that startup MongoDB instance
165 -
    const instance = await MongoInstance.run({
166 -
      instance: {
167 -
        dbPath: data.dbPath,
168 -
        ip: data.ip,
169 -
        port: data.port,
170 -
        storageEngine: data.storageEngine,
171 -
        replSet: data.replSet,
172 -
        args: instOpts.args,
173 -
        auth: instOpts.auth,
174 -
      },
175 -
      binary: this.opts.binary,
176 -
      spawn: this.opts.spawn,
177 -
    });
401 +
    let instance = await MongoInstance.run(mongodOptions);
402 +
403 +
    // another "isNullOrUndefined" because otherwise typescript complains about "this.auth" possibly being not defined
404 +
    if (!isNullOrUndefined(this.auth) && createAuth) {
405 +
      log(`Running "createAuth" (force: "${this.auth.force}")`);
406 +
      await this.createAuth(data);
407 +
408 +
      if (data.storageEngine !== 'ephemeralForTest') {
409 +
        log('Killing No-Auth instance');
410 +
        await instance.kill();
411 +
412 +
        // TODO: change this to just change the options instead of an new instance after adding getters & setters
413 +
        log('Starting Auth Instance');
414 +
        instance = await MongoInstance.run({
415 +
          ...mongodOptions,
416 +
          instance: {
417 +
            ...mongodOptions.instance,
418 +
            auth: true,
419 +
          },
420 +
        });
421 +
      } else {
422 +
        console.warn(
423 +
          'Not Restarting MongoInstance for Auth\n' +
424 +
            'Storage engine is ephemeralForTest, which does not write data on shutdown, and mongodb does not allow changeing "auth" runtime'
425 +
        );
426 +
      }
427 +
    } else {
428 +
      // extra "if" to log when "disable" is set to "true"
429 +
      if (this.opts.auth?.disable) {
430 +
        log('AutomaticAuth.disable is set to "true" skipping "createAuth"');
431 +
      }
432 +
    }
178 433
179 -
    return {
434 +
    this._instanceInfo = {
180 435
      ...data,
181 436
      dbPath: data.dbPath as string, // because otherwise the types would be incompatible
182 -
      instance: instance,
437 +
      instance,
183 438
    };
184 439
  }
185 440
186 441
  /**
187 442
   * Stop the current In-Memory Instance
443 +
   * @param runCleanup run "this.cleanup"? (remove dbPath & reset "instanceInfo")
188 444
   */
189 -
  async stop(): Promise<boolean> {
445 +
  async stop(runCleanup: boolean = true): Promise<boolean> {
190 446
    log('Called MongoMemoryServer.stop() method');
191 447
192 -
    // just return "true" if the instance is already running / defined
448 +
    // just return "true" if there was never an instance
193 449
    if (isNullOrUndefined(this._instanceInfo)) {
194 -
      log('Instance is already stopped, returning true');
450 +
      log('"instanceInfo" is not defined (never ran?)');
451 +
      return true;
452 +
    }
453 +
454 +
    if (this._state === MongoMemoryServerStates.stopped) {
455 +
      log(`stop: state is "stopped", so already stopped`);
195 456
      return true;
196 457
    }
197 458
@@ -208,52 +469,116 @@
Loading
208 469
    );
209 470
    await this._instanceInfo.instance.kill();
210 471
472 +
    this.stateChange(MongoMemoryServerStates.stopped);
473 +
474 +
    if (runCleanup) {
475 +
      await this.cleanup(false);
476 +
    }
477 +
478 +
    return true;
479 +
  }
480 +
481 +
  /**
482 +
   * Remove the defined dbPath
483 +
   * This function gets automatically called on process event "beforeExit" (with force being "false")
484 +
   * @param force Remove the dbPath even if it is no "tmpDir" (and re-check if tmpDir actually removed it)
485 +
   * @throws If "state" is not "stopped"
486 +
   * @throws If "instanceInfo" is not defined
487 +
   * @throws If an fs error occured
488 +
   */
489 +
  async cleanup(force: boolean): Promise<void>;
490 +
  /**
491 +
   * This Overload is used for the "beforeExit" listener (ignore this)
492 +
   * @internal
493 +
   */
494 +
  async cleanup(code?: number): Promise<void>;
495 +
  async cleanup(force: boolean | number = false): Promise<void> {
496 +
    if (typeof force !== 'boolean') {
497 +
      force = false;
498 +
    }
499 +
    assertion(
500 +
      this.state === MongoMemoryServerStates.stopped,
501 +
      new Error('Cannot cleanup when state is not "stopped"')
502 +
    );
503 +
    process.removeListener('beforeExit', this.cleanup);
504 +
    if (isNullOrUndefined(this._instanceInfo)) {
505 +
      log('cleanup: "instanceInfo" is undefined');
506 +
      return;
507 +
    }
508 +
    assertion(
509 +
      isNullOrUndefined(this._instanceInfo.instance.childProcess),
510 +
      new Error('Cannot cleanup because "instance.childProcess" is still defined')
511 +
    );
512 +
513 +
    log(`cleanup: force ${force}`);
514 +
211 515
    const tmpDir = this._instanceInfo.tmpDir;
212 -
    if (tmpDir) {
213 -
      log(`Removing tmpDir ${tmpDir.name}`);
516 +
    if (!isNullOrUndefined(tmpDir)) {
517 +
      log(`cleanup: removing tmpDir at ${tmpDir.name}`);
214 518
      tmpDir.removeCallback();
215 519
    }
216 520
217 -
    this._instanceInfo = undefined;
218 -
    this.stateChange(MongoMemoryServerStateEnum.stopped);
521 +
    if (force) {
522 +
      const dbPath: string = this._instanceInfo.dbPath;
523 +
      const res = await statPath(dbPath);
524 +
525 +
      if (isNullOrUndefined(res)) {
526 +
        log(`cleanup: force is true, but path "${dbPath}" dosnt exist anymore`);
527 +
      } else {
528 +
        assertion(res.isDirectory(), new Error('Defined dbPath is not an directory'));
529 +
530 +
        if (lt(process.version, '12.10.0')) {
531 +
          try {
532 +
            const rimraf = (await import('rimraf')).sync;
533 +
            rimraf(dbPath);
534 +
          } catch (err) {
535 +
            console.warn('When using NodeJS below 12.10 package "rimraf" is needed');
536 +
            throw err;
537 +
          }
538 +
        } else {
539 +
          await fspromises.rmdir(dbPath, { recursive: true, maxRetries: 1 });
540 +
        }
541 +
      }
542 +
    }
219 543
220 -
    return true;
544 +
    this.stateChange(MongoMemoryServerStates.new); // reset "state" to new, because the dbPath got removed
545 +
    this._instanceInfo = undefined;
221 546
  }
222 547
223 548
  /**
224 549
   * Get Information about the currently running instance, if it is not running it returns "undefined"
225 550
   */
226 -
  get instanceInfo(): MongoInstanceDataT | undefined {
551 +
  get instanceInfo(): MongoInstanceData | undefined {
227 552
    return this._instanceInfo;
228 553
  }
229 554
230 555
  /**
231 556
   * Get Current state of this class
232 557
   */
233 -
  get state(): MongoMemoryServerStateEnum {
558 +
  get state(): MongoMemoryServerStates {
234 559
    return this._state;
235 560
  }
236 561
237 562
  /**
238 563
   * Ensure that the instance is running
239 564
   * -> throws if instance cannot be started
240 565
   */
241 -
  async ensureInstance(): Promise<MongoInstanceDataT> {
566 +
  async ensureInstance(): Promise<MongoInstanceData> {
242 567
    log('Called MongoMemoryServer.ensureInstance() method');
243 -
    if (this._instanceInfo) {
244 -
      return this._instanceInfo;
245 -
    }
246 568
247 569
    switch (this._state) {
248 -
      case MongoMemoryServerStateEnum.running:
570 +
      case MongoMemoryServerStates.running:
571 +
        if (this._instanceInfo) {
572 +
          return this._instanceInfo;
573 +
        }
249 574
        throw new Error('MongoMemoryServer "_state" is "running" but "instanceInfo" is undefined!');
250 -
      case MongoMemoryServerStateEnum.new:
251 -
      case MongoMemoryServerStateEnum.stopped:
575 +
      case MongoMemoryServerStates.new:
576 +
      case MongoMemoryServerStates.stopped:
252 577
        break;
253 -
      case MongoMemoryServerStateEnum.starting:
578 +
      case MongoMemoryServerStates.starting:
254 579
        return new Promise((res, rej) =>
255 -
          this.once(MongoMemoryServerEventEnum.stateChange, (state) => {
256 -
            if (state != MongoMemoryServerStateEnum.running) {
580 +
          this.once(MongoMemoryServerEvents.stateChange, (state) => {
581 +
            if (state != MongoMemoryServerStates.running) {
257 582
              rej(
258 583
                new Error(
259 584
                  `"ensureInstance" waited for "running" but got an different state: "${state}"`
@@ -267,7 +592,7 @@
Loading
267 592
        throw new Error(`"ensureInstance" does not have an case for "${this._state}"`);
268 593
    }
269 594
270 -
    log(' - no running instance, call `start()` command');
595 +
    log(' - no running instance, calling `start()` command');
271 596
    await this.start();
272 597
    log(' - `start()` command was succesfully resolved');
273 598
@@ -294,7 +619,72 @@
Loading
294 619
      dbName = typeof otherDbName === 'string' ? otherDbName : generateDbName();
295 620
    }
296 621
297 -
    return getUriBase(this._instanceInfo.ip, this._instanceInfo.port, dbName);
622 +
    return uriTemplate(this._instanceInfo.ip, this._instanceInfo.port, dbName);
623 +
  }
624 +
625 +
  /**
626 +
   * Create Users and restart instance to enable auth
627 +
   * This Function assumes "this.opts.auth" is defined / enabled
628 +
   * @param data Used to get "ip" and "port"
629 +
   *
630 +
   * @internal
631 +
   */
632 +
  async createAuth(data: StartupInstanceData): Promise<void> {
633 +
    assertion(
634 +
      !isNullOrUndefined(this.auth),
635 +
      new Error('"createAuth" got called, but "this.auth" is undefined!')
636 +
    );
637 +
    log('createAuth, options:', this.auth);
638 +
    const con: MongoClient = await MongoClient.connect(uriTemplate(data.ip, data.port, 'admin'), {
639 +
      useNewUrlParser: true,
640 +
      useUnifiedTopology: true,
641 +
    });
642 +
643 +
    let db = con.db('admin'); // just to ensure it is actually the "admin" database AND to have the "Db" data
644 +
645 +
    // Create the root user
646 +
    log(`Creating Root user, name: "${this.auth.customRootName}"`);
647 +
    await db.command({
648 +
      createUser: this.auth.customRootName,
649 +
      pwd: 'rootuser',
650 +
      mechanisms: ['SCRAM-SHA-256'],
651 +
      customData: {
652 +
        createBy: 'mongodb-memory-server',
653 +
        as: 'ROOTUSER',
654 +
      },
655 +
      roles: ['root'],
656 +
    } as CreateUserMongoDB);
657 +
658 +
    if (this.auth.extraUsers.length > 0) {
659 +
      log(`Creating "${this.auth.extraUsers.length}" Custom Users`);
660 +
      this.auth.extraUsers.sort((a, b) => {
661 +
        if (a.database === 'admin') {
662 +
          return -1; // try to make all "admin" at the start of the array
663 +
        }
664 +
        return a.database === b.database ? 0 : 1; // "0" to sort same databases continuesly, "-1" if nothing before/above applies
665 +
      });
666 +
667 +
      for (const user of this.auth.extraUsers) {
668 +
        user.database = isNullOrUndefined(user.database) ? 'admin' : user.database;
669 +
        // just to have not to call "con.db" everytime in the loop if its the same
670 +
        if (user.database !== db.databaseName) {
671 +
          db = con.db(user.database);
672 +
        }
673 +
674 +
        log('Creating User: ', user);
675 +
        await db.command({
676 +
          createUser: user.createUser,
677 +
          pwd: user.pwd,
678 +
          customData: user.customData ?? {},
679 +
          roles: user.roles,
680 +
          authenticationRestrictions: user.authenticationRestrictions ?? [],
681 +
          mechanisms: user.mechanisms ?? ['SCRAM-SHA-256'],
682 +
          digestPassword: user.digestPassword ?? true,
683 +
        } as CreateUserMongoDB);
684 +
      }
685 +
    }
686 +
687 +
    await con.close();
298 688
  }
299 689
}
300 690
@@ -305,6 +695,6 @@
Loading
305 695
 * -> this couldnt be included in the class, because "asserts this.instanceInfo" is not allowed
306 696
 * @param val this.instanceInfo
307 697
 */
308 -
function assertionInstanceInfo(val: unknown): asserts val is MongoInstanceDataT {
698 +
function assertionInstanceInfo(val: unknown): asserts val is MongoInstanceData {
309 699
  assertion(!isNullOrUndefined(val), new Error('"instanceInfo" is undefined'));
310 700
}

@@ -3,18 +3,37 @@
Loading
3 3
import path from 'path';
4 4
import MongoBinary from './MongoBinary';
5 5
import { MongoBinaryOpts } from './MongoBinary';
6 -
import { StorageEngineT } from '../types';
7 6
import debug from 'debug';
8 -
import { assertion, isNullOrUndefined, killProcess } from './db_util';
7 +
import { assertion, uriTemplate, isNullOrUndefined, killProcess } from './utils';
9 8
import { lt } from 'semver';
10 9
import { EventEmitter } from 'events';
10 +
import { MongoClient, MongoNetworkError } from 'mongodb';
11 11
12 12
if (lt(process.version, '10.15.0')) {
13 13
  console.warn('Using NodeJS below 10.15.0');
14 14
}
15 15
16 16
const log = debug('MongoMS:MongoInstance');
17 17
18 +
export type StorageEngine = 'devnull' | 'ephemeralForTest' | 'mmapv1' | 'wiredTiger';
19 +
20 +
export interface MongoMemoryInstancePropBase {
21 +
  args?: string[];
22 +
  port?: number | null;
23 +
  dbPath?: string;
24 +
  storageEngine?: StorageEngine;
25 +
}
26 +
27 +
// TODO: find an better name for this interface
28 +
// TODO: find a way to unify with "MongoInstanceOpts"
29 +
export interface MongoMemoryInstanceProp extends MongoMemoryInstancePropBase {
30 +
  auth?: boolean;
31 +
  dbName?: string;
32 +
  ip?: string; // for binding to all IP addresses set it to `::,0.0.0.0`, by default '127.0.0.1'
33 +
  replSet?: string;
34 +
  storageEngine?: StorageEngine;
35 +
}
36 +
18 37
export enum MongoInstanceEvents {
19 38
  instanceReplState = 'instanceReplState',
20 39
  instancePrimary = 'instancePrimary',
@@ -34,7 +53,7 @@
Loading
34 53
export interface MongoInstanceOpts {
35 54
  port?: number;
36 55
  ip?: string; // for binding to all IP addresses set it to `::,0.0.0.0`, by default '127.0.0.1'
37 -
  storageEngine?: StorageEngineT;
56 +
  storageEngine?: StorageEngine;
38 57
  dbPath?: string;
39 58
  replSet?: string;
40 59
  args?: string[];
@@ -66,7 +85,7 @@
Loading
66 85
export class MongoInstance extends EventEmitter {
67 86
  // Mark these values as "readonly" & "Readonly" because modifying them after starting will have no effect
68 87
  // readonly is required otherwise the property can still be changed on the root level
69 -
  readonly instanceOpts: Readonly<MongoInstanceOpts>;
88 +
  instanceOpts: MongoInstanceOpts;
70 89
  readonly binaryOpts: Readonly<MongoBinaryOpts>;
71 90
  readonly spawnOpts: Readonly<SpawnOptions>;
72 91
@@ -86,6 +105,10 @@
Loading
86 105
   * This boolean is "true" if the instance is successfully started
87 106
   */
88 107
  isInstanceReady: boolean = false;
108 +
  /**
109 +
   * This boolean is "true" if the instance is part of an replset
110 +
   */
111 +
  isReplSet: boolean = false;
89 112
90 113
  constructor(opts: Partial<MongodOpts>) {
91 114
    super();
@@ -154,6 +177,7 @@
Loading
154 177
      result.push('--noauth');
155 178
    }
156 179
    if (!!this.instanceOpts.replSet) {
180 +
      this.isReplSet = true;
157 181
      result.push('--replSet', this.instanceOpts.replSet);
158 182
    }
159 183
@@ -169,6 +193,10 @@
Loading
169 193
   * @fires MongoInstance#instanceStarted
170 194
   */
171 195
  async run(): Promise<this> {
196 +
    this.isInstancePrimary = false;
197 +
    this.isInstanceReady = false;
198 +
    this.isReplSet = false;
199 +
172 200
    const launch: Promise<void> = new Promise((resolve, reject) => {
173 201
      this.once(MongoInstanceEvents.instanceReady, resolve);
174 202
      this.once(MongoInstanceEvents.instanceError, reject);
@@ -193,6 +221,51 @@
Loading
193 221
    this.debug('Called MongoInstance.kill():');
194 222
195 223
    if (!isNullOrUndefined(this.childProcess)) {
224 +
      // try to run "replSetStepDown" before running "killProcess" (gracefull "SIGINT")
225 +
      // running "&& this.isInstancePrimary" otherwise "replSetStepDown" will fail with "MongoError: not primary so can't step down"
226 +
      if (this.isReplSet && this.isInstancePrimary) {
227 +
        let con: MongoClient | undefined;
228 +
        try {
229 +
          log('kill: instanceStopFailed event');
230 +
          const port = this.instanceOpts.port;
231 +
          const ip = this.instanceOpts.ip;
232 +
          assertion(
233 +
            !isNullOrUndefined(port),
234 +
            new Error('Cannot shutdown replset gracefully, no "port" is provided')
235 +
          );
236 +
          assertion(
237 +
            !isNullOrUndefined(ip),
238 +
            new Error('Cannot shutdown replset gracefully, no "ip" is provided')
239 +
          );
240 +
241 +
          con = await MongoClient.connect(uriTemplate(ip, port, 'admin'), {
242 +
            useNewUrlParser: true,
243 +
            useUnifiedTopology: true,
244 +
          });
245 +
246 +
          const admin = con.db('admin'); // just to ensure it is actually the "admin" database
247 +
          await admin.command({ replSetStepDown: 1, force: true });
248 +
          await con.close();
249 +
        } catch (err) {
250 +
          // Quote from MongoDB Documentation (https://docs.mongodb.com/manual/reference/command/replSetStepDown/#client-connections):
251 +
          // > Starting in MongoDB 4.2, replSetStepDown command no longer closes all client connections.
252 +
          // > In MongoDB 4.0 and earlier, replSetStepDown command closes all client connections during the step down.
253 +
          // so error "MongoNetworkError: connection 1 to 127.0.0.1:41485 closed" will get thrown below 4.2
254 +
          if (
255 +
            !(
256 +
              err instanceof MongoNetworkError &&
257 +
              /^connection \d+ to [\d.]+:\d+ closed$/i.test(err.message)
258 +
            )
259 +
          ) {
260 +
            console.warn(err);
261 +
          }
262 +
        } finally {
263 +
          if (!isNullOrUndefined(con)) {
264 +
            // even if it errors out, somehow the connection stays open
265 +
            await con.close();
266 +
          }
267 +
        }
268 +
      }
196 269
      await killProcess(this.childProcess, 'childProcess');
197 270
      this.childProcess = undefined; // reset reference to the childProcess for "mongod"
198 271
    } else {
@@ -296,8 +369,11 @@
Loading
296 369
   * @fires MongoInstance#instanceClosed
297 370
   */
298 371
  closeHandler(code: number): void {
299 -
    if (code != 0) {
300 -
      this.debug('Mongod instance closed with an non-0 code!');
372 +
    // check if the platform is windows, if yes check if the code is not "12" or "0" otherwise just check code is not "0"
373 +
    // because for mongodb any event on windows (like SIGINT / SIGTERM) will result in an code 12
374 +
    // https://docs.mongodb.com/manual/reference/exit-codes/#12
375 +
    if ((process.platform === 'win32' && code != 12 && code != 0) || code != 0) {
376 +
      this.debug('Mongod instance closed with an non-0 (or non 12 on windows) code!');
301 377
    }
302 378
    this.debug(`CLOSE: ${code}`);
303 379
    this.emit(MongoInstanceEvents.instanceClosed, code);

@@ -7,15 +7,16 @@
Loading
7 7
import { execSync } from 'child_process';
8 8
import { promisify } from 'util';
9 9
import MongoBinaryDownload from './MongoBinaryDownload';
10 -
import resolveConfig, { envToBool } from './resolve-config';
10 +
import resolveConfig, { envToBool } from './resolveConfig';
11 11
import debug from 'debug';
12 +
import { assertion } from './utils';
12 13
13 14
const log = debug('MongoMS:MongoBinary');
14 15
15 16
// TODO: return back `latest` version when it will be fixed in MongoDB distro (for now use 4.0.14 😂)
16 17
// More details in https://github.com/nodkz/mongodb-memory-server/issues/131
17 18
// export const LATEST_VERSION = 'latest';
18 -
export const LATEST_VERSION: string = '4.0.14';
19 +
export const LATEST_VERSION: string = '4.0.20';
19 20
20 21
export interface MongoBinaryCache {
21 22
  [version: string]: string;
@@ -30,7 +31,7 @@
Loading
30 31
}
31 32
32 33
export class MongoBinary {
33 -
  static cache: MongoBinaryCache = {};
34 +
  static cache: Map<string, string> = new Map();
34 35
35 36
  /**
36 37
   * Probe if the provided "systemBinary" is an existing path
@@ -56,8 +57,8 @@
Loading
56 57
   * Check if specified version already exists in the cache
57 58
   * @param version The Version to check for
58 59
   */
59 -
  static getCachePath(version: string): string {
60 -
    return this.cache[version];
60 +
  static getCachePath(version: string): string | undefined {
61 +
    return this.cache.get(version);
61 62
  }
62 63
63 64
  /**
@@ -100,7 +101,7 @@
Loading
100 101
        version,
101 102
        checkMD5,
102 103
      });
103 -
      this.cache[version] = await downloader.getMongodPath();
104 +
      this.cache.set(version, await downloader.getMongodPath());
104 105
    }
105 106
    // remove lock
106 107
    await new Promise((res) => {
@@ -113,7 +114,14 @@
Loading
113 114
        res(); // we don't care if it was successful or not
114 115
      });
115 116
    });
116 -
    return this.getCachePath(version);
117 +
118 +
    const cachePath = this.getCachePath(version);
119 +
    // ensure that "path" exists, so the return type does not change
120 +
    assertion(
121 +
      typeof cachePath === 'string',
122 +
      new Error(`No Cache Path for version "${version}" found (and download failed silently?)`)
123 +
    );
124 +
    return cachePath;
117 125
  }
118 126
119 127
  /**
@@ -155,7 +163,7 @@
Loading
155 163
    const options = { ...defaultOptions, ...opts };
156 164
    log(`MongoBinary options:`, JSON.stringify(options, null, 2));
157 165
158 -
    let binaryPath = '';
166 +
    let binaryPath: string | undefined;
159 167
160 168
    if (options.systemBinary) {
161 169
      binaryPath = await this.getSystemPath(options.systemBinary);

@@ -5,7 +5,7 @@
Loading
5 5
const log = debug('MongoMS:ResolveConfig');
6 6
7 7
const ENV_CONFIG_PREFIX = 'MONGOMS_';
8 -
const defaultValues = new Map<string, string>();
8 +
export const defaultValues = new Map<string, string>();
9 9
10 10
/**
11 11
 * Set an Default value for an specific key

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Unable to process changes.

No base report to compare against.

140 Commits

Hiding 1 contexual commits
-9
-5
-4
Hiding 2 contexual commits
+1
+2
-1
+3
+5
-2
Hiding 6 contexual commits
+7
+7
Hiding 2 contexual commits
Hiding 11 contexual commits
+11
+18
-7
Hiding 1 contexual commits
+48
+36
+12
Hiding 4 contexual commits
+13
+12
+1
+8
+5
+3
Hiding 1 contexual commits
+82
+75
+7
Hiding 2 contexual commits
Hiding 2 contexual commits
+1
+1
Hiding 5 contexual commits
+4
+3
+1
Hiding 2 contexual commits
-8
-11
+3
Hiding 72 contexual commits
+49
+65
-16
+1
-1
Hiding 7 contexual commits
+3
-4
+7
Pull Request Base Commit
Files Coverage
packages/mongodb-memory-server-core/src 83.94%
Project Totals (9 files) 83.94%
Loading