Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
RichardBoyewa committed Aug 8, 2023
0 parents commit f01a69c
Show file tree
Hide file tree
Showing 8 changed files with 4,157 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
test.*
dist/.mongot
node_modules
88 changes: 88 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Mongohat
Simple MongoDB testing library for NodeJs applications. Inspired by [`mongo-unit`](https://www.npmjs.com/package/mongo-unit).
It works with `mongodb-memory-server` as a main dependency.
The major focus of the package is to simplify the loading of seed data for each test suite, and can refresh the
the database to bring back its initial state before the test suite making it easy to test several scenario before calling a `.refresh()`
on the `Mongohat` instance.

## Mongohat methods
The following methods are exposed to make test writing easy and fun again :)

### New Mongohat Instance
Every instance created can connect to existing database, and if the database name does not exist it creates
a new one.
Do not call this in a loop.
```js
MongohatOption {
dbName: string;
dbPath: string;
dbPort?: number;
useReplicaSet?: boolean;
version?: string;
}
const mongohat = new Mongohat("DATEBASE-NAME", option);
```
`option` here is of type `MongohatOption`

### `.start(verbose)`
This method starts the in-memory server instance. If `verbose` is true then thelogs from `mongodb-memory-server` are directly piped to the
console output
### `.load([{...}, {...}], retainPreviousData = false)`
This loads in the initial data into the in-memory server. If `retainPreviousData` is set to true (default is false), then the loaded data is
added to the existing data, otherwise it overrides it giving `Mongohat` a new state.
```js
await mongohat.load({
inventory: [
{
productName: "test",
qty: 5
},
{
productName: "test",
qty: 2
},
...
{
productName: "John",
qty: 8
},
],
products: [
{
...
},
],
});
```

### `.getCollection(collectionName)`
This returns a collection object of type `Collection<Document>`, and can used to further interact with the collection
```js
const inventory = mongohat.getCollection("inventory");
await inventory.insertOne({
_id: new ObjectId("56d9bf92f9be48771d6fe5b1"),
productName: "Collection Insert",
qty: 78,
} as unknown as Document);

```
### `.refresh()`
This method can be called in the `afterEach` or `beforeEach` hook of the test suite (or as applicable to your scenario).
```js
beforeEach(async() => {
await mongohat.refresh();
})
```
It reverts the state of the test data to the state after the last `.load()` was called.

### `.getDBUrl()`
This method returns the dynamic connection string assigned to the instance of the mongodb running in-memory.
If called before a client is well instantiated it throws an exception.
This should be called after the `.start()` method has completed.
```js
const mongohat = new Mongohat("<DATABASE-NAME>");
await mongohat.start(false);
...
process.env.DB_URL = mongohat.getDBUrl()
// refresh your config here
```
40 changes: 40 additions & 0 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Collection, MongoClient } from "mongodb";
import { MongoMemoryServer, MongoMemoryReplSet } from "mongodb-memory-server";
export interface MongohatOption {
dbName: string;
dbPath: string;
dbPort?: number;
useReplicaSet?: boolean;
version?: string;
}
export declare class Mongohat {
protected context: string;
protected dataDir: string;
protected defaultTempDir: string;
protected defaultPort: number;
protected config: MongohatOption;
protected mongod: MongoMemoryServer | MongoMemoryReplSet;
protected dbUrl: string;
protected debug: any;
protected testData: any;
protected client: MongoClient;
/**
*
*/
constructor(contextName: string, option?: MongohatOption);
private initMongo;
start(verbose: boolean): Promise<void> | Promise<string>;
private checkTempDirExist;
private killPreviousMongoProcess;
private getFreePort;
private prepareMongoOptions;
load(data: any, retainPreviousData?: boolean): Promise<import("mongodb").InsertManyResult<import("bson").Document>[]>;
getCollection(collectionName: string): Collection<Document>;
refresh(): Promise<import("mongodb").InsertManyResult<import("bson").Document>[]>;
clean(data?: {}): Promise<(boolean | void)[]>;
drop(): Promise<boolean>;
private dropDB;
private delay;
getDBUrl(): string;
stop(): Promise<void>;
}
241 changes: 241 additions & 0 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Mongohat = void 0;
const mongodb_1 = require("mongodb");
const mongodb_memory_server_1 = require("mongodb-memory-server");
const fs = require("fs");
const ps = require("ps-node");
const Debug = require("debug");
const portfinder = require("portfinder");
class Mongohat {
/**
*
*/
constructor(contextName, option) {
this.dataDir = "/.Mongohat";
this.defaultPort = 27777;
this.context = contextName;
this.defaultTempDir = `${__dirname}${this.dataDir}`;
this.config = {
dbName: this.context && this.context.trim().length > 0
? this.context
: `Mongohat-test`,
dbPath: this.defaultTempDir,
dbPort: this.defaultPort,
useReplicaSet: false,
};
this.debug = Debug("Mongohat");
if (option)
this.config = Object.assign(Object.assign({}, this.config), option);
}
initMongo() {
return __awaiter(this, void 0, void 0, function* () {
this.mongod = this.config.useReplicaSet
? yield mongodb_memory_server_1.MongoMemoryReplSet.create(this.prepareMongoOptions())
: yield mongodb_memory_server_1.MongoMemoryServer.create(this.prepareMongoOptions());
this.dbUrl = this.mongod.getUri();
this.client = yield mongodb_1.MongoClient.connect(this.dbUrl, {
useUnifiedTopology: true,
});
this.debug(`Mongohat DB connection accessible via ${this.dbUrl}`);
});
}
start(verbose) {
if (verbose) {
Debug.enable("Mongohat");
Debug.enable("*");
}
this.debug("Starting Mongohat...");
if (this.dbUrl) {
return Promise.resolve(this.dbUrl);
}
this.checkTempDirExist(this.defaultTempDir);
return this.killPreviousMongoProcess(this.defaultTempDir)
.then(() => this.getFreePort(this.config.dbPort))
.then(() => this.initMongo());
}
checkTempDirExist(dir) {
try {
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true });
}
fs.mkdirSync(dir);
}
catch (error) {
console.error("Unable to create db folder", dir, error);
if (error.code !== "EEXIST") {
throw error;
}
}
}
killPreviousMongoProcess(dataPath) {
return new Promise((resolve, reject) => {
ps.lookup({
psargs: ["-A"],
command: "mongod",
arguments: dataPath,
}, (err, resultList) => {
if (err) {
console.log("ps-node error", err);
return reject(err);
}
resultList.forEach((process) => {
if (process) {
console.log("KILL PID: %s, COMMAND: %s, ARGUMENTS: %s", process.pid, process.command, process.arguments);
ps.kill(process.pid);
}
});
return resolve();
});
});
}
getFreePort(possiblePort) {
portfinder.setBasePort(possiblePort);
return new Promise((resolve, reject) => portfinder.getPort((err, port) => {
if (err) {
this.debug(`cannot get free port: ${err}`);
reject(err);
}
else {
resolve(port);
}
}));
}
prepareMongoOptions() {
const mongoOption = {
autoStart: false,
};
if (this.config.version) {
mongoOption.binary = { version: this.config.version };
}
if (this.config.useReplicaSet) {
mongoOption.instanceOpts = [
{
port: this.config.dbPort,
dbPath: this.config.dbPath,
storageEngine: "wiredTiger",
},
];
mongoOption.replSet = {
dbName: this.config.dbName,
storageEngine: "wiredTiger",
};
}
else {
mongoOption.instance = {
port: this.config.dbPort,
dbPath: this.config.dbPath,
dbName: this.config.dbName,
storageEngine: "ephemeralForTest",
};
}
return mongoOption;
}
load(data, retainPreviousData = false) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.client) {
throw new Error("The client has not been instantiated.");
}
if (!retainPreviousData) {
yield this.clean(data);
}
this.testData = data;
const db = this.client.db(this.config.dbName);
const queries = Object.keys(data).map((col) => {
const collection = db.collection(col);
return collection.insertMany(data[col]);
});
return Promise.all(queries);
});
}
getCollection(collectionName) {
if (!this.client) {
throw new Error("The client has not been instantiated.");
}
const db = this.client.db(this.config.dbName);
return db.collection(collectionName);
}
refresh() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.testData || Object.keys(this.testData).length === 0) {
console.info("Test Data is empty. Nothing to refresh.");
return;
}
return this.load(this.testData);
});
}
clean(data = {}) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.client) {
throw new Error("The client has not been instantiated.");
}
this.testData = {};
if (!data || Object.keys(data).length === 0) {
return this.dropDB();
}
const db = this.client.db(this.config.dbName);
const queries = Object.keys(data).map((col) => {
const collection = db.collection(col);
return collection
.drop()
.catch((e) => console.info("Info: Collection not found.", col));
});
return Promise.all(queries);
});
}
drop() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.client) {
throw new Error("The client has not been instantiated.");
}
this.testData = {};
return this.client.db(this.config.dbName).dropDatabase();
});
}
dropDB() {
return __awaiter(this, void 0, void 0, function* () {
this.testData = {};
const db = this.client.db(this.config.dbName);
return db.collections().then((collections) => {
const requests = collections.map((col) => col
.drop()
.catch((e) => console.info("Info: Collection not found.", col)));
return Promise.all(requests);
});
});
}
delay(time) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise(resolve => setTimeout(resolve, time));
});
}
getDBUrl() {
if (!this.client) {
throw new Error("The client has not been instantiated.");
}
return this.dbUrl;
}
stop() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.client) {
throw new Error("The client has not been instantiated.");
}
yield this.client.close(true);
yield this.mongod.stop(true);
this.dbUrl = null;
console.log('Killing MongoDB process...');
yield this.killPreviousMongoProcess(this.defaultTempDir);
yield this.delay(100);
});
}
}
exports.Mongohat = Mongohat;
Loading

0 comments on commit f01a69c

Please sign in to comment.