UNPKG

btrz-simple-dao

Version:
1,004 lines (867 loc) 37.4 kB
// eslint-disable-next-line max-statements describe("SimpleDao", () => { const Chance = require("chance"); const chance = new Chance(); const { ObjectID, MongoClient, Cursor } = require("mongodb"); const chai = require("chai"); const expect = chai.expect; const sinon = require("sinon"); const sandbox = sinon.createSandbox(); const { ALL_AUTH_MECHANISMS, ALL_READ_PREFERENCES } = require("../constants"); const SimpleDao = require("../").SimpleDao; const { getConnectionString } = require("../src/simple-dao"); async function databaseHasCollection(db, collectionName) { const allCollections = await db.listCollections().toArray(); return allCollections.some((collection) => { return collection.name === collectionName; }); } let config = null; let simpleDao = null; let collectionName = null; let model = null; class Model { static collectionName() { return collectionName; } static factory(literal) { return Object.assign(new Model(), literal); } } async function expectDocumentDoesNotExist(id, _collectionName = collectionName) { const db = await simpleDao.connect(); const document = await db.collection(_collectionName).findOne({_id: id}); expect(document).to.not.exist; } beforeEach(() => { config = { db: { options: { database: "simple_dao_test", username: "", password: "" }, uris: ["127.0.0.1:27017"] } }; collectionName = chance.word({length: 10}); model = Model.factory({a: 1}); simpleDao = new SimpleDao(config); }); afterEach(async () => { sandbox.restore(); const db = await simpleDao.connect(); try { await db.dropCollection(collectionName); } catch (err) { // ignore error } }); describe(".objectId()", () => { describe("static method", () => { it("should return a new objectId", () => { expect(SimpleDao.objectId()).to.be.an.instanceOf(ObjectID); }); it("should return an objectId from the given 24 characters argument", () => { const id = "55b27c2a74757b3c5e121b0e"; expect(SimpleDao.objectId(id).toString()).to.be.eql(id); }); }); describe("instance method", () => { it("should return a new objectId", () => { expect(simpleDao.objectId()).to.be.an.instanceOf(ObjectID); }); it("should return an objectId from the given 24 characters argument", () => { const id = "55b27c2a74757b3c5e121b0e"; expect(simpleDao.objectId(id).toString()).to.be.eql(id); }); }); }); describe("getConnectionString()", () => { it("should return a valid connection string for one db server", () => { const connectionString = getConnectionString(config.db); expect(connectionString).to.eql("mongodb://127.0.0.1:27017/simple_dao_test"); }); it("should not include an authentication mechanism if no username or pwd", () => { const connectionString = getConnectionString(config.db); expect(connectionString).to.eql("mongodb://127.0.0.1:27017/simple_dao_test"); }); it("should return a valid connection string for one db server using authentication credentials", () => { const config2 = { db: { options: { database: "simple_dao_test", username: "usr", password: "pwd" }, uris: ["127.0.0.1:27017"] } }; const connectionString = getConnectionString(config2.db); expect(connectionString).to.eql("mongodb://usr:pwd@127.0.0.1:27017/simple_dao_test?authMechanism=DEFAULT"); }); it("should URL-encode the authentication credentials " + "so that credentials that include symbols will not result in invalid connection strings", () => { const config2 = { db: { options: { database: "simple_dao_test", username: "u$ername", password: "pa$$w{}rd" }, uris: ["127.0.0.1:27017"] } }; const connectionString = getConnectionString(config2.db); expect(connectionString) .to.eql("mongodb://u%24ername:pa%24%24w%7B%7Drd@127.0.0.1:27017/simple_dao_test?authMechanism=DEFAULT"); }); it("should return a valid connection string for many db servers using authentication credentials", () => { const config2 = { db: { options: { database: "simple_dao_test", username: "usr", password: "pwd" }, uris: [ "127.0.0.1:27017", "127.0.0.2:27018" ] } }; const connectionString = getConnectionString(config2.db); expect(connectionString).to.eql("mongodb://usr:pwd@127.0.0.1:27017,127.0.0.2:27018/simple_dao_test?authMechanism=DEFAULT"); }); it("should return a valid connection string that includes the specified authentication mechanism", () => { for (const authMechanism of ALL_AUTH_MECHANISMS) { const config2 = { db: { options: { database: "simple_dao_test", username: "usr", password: "pwd", authMechanism }, uris: ["127.0.0.1:27017"] } }; const connectionString = getConnectionString(config2.db); expect(connectionString).to.eql(`mongodb://usr:pwd@127.0.0.1:27017/simple_dao_test?authMechanism=${authMechanism}`); } }); it("should throw an error if an invalid authentication mechanism is specified", () => { const config2 = { db: { options: { database: "simple_dao_test", username: "usr", password: "pwd", authMechanism: "some_invalid_auth_mechanism" }, uris: ["127.0.0.1:27017"] } }; expect(() => { return getConnectionString(config2.db); }) .to.throw("Database config 'authMechanism' must be one of DEFAULT, MONGODB-CR, SCRAM-SHA-1, SCRAM-SHA-256"); }); it("should return a valid connection string that includes the specified read preference", () => { for (const readPreference of ALL_READ_PREFERENCES) { const config2 = { db: { options: { database: "simple_dao_test", username: "usr", password: "pwd", readPreference }, uris: ["127.0.0.1:27017"] } }; const connectionString = getConnectionString(config2.db); expect(connectionString) .to.eql(`mongodb://usr:pwd@127.0.0.1:27017/simple_dao_test?authMechanism=DEFAULT&readPreference=${readPreference}`); } }); it("should throw an error if an invalid read preference is specified", () => { const config2 = { db: { options: { database: "simple_dao_test", username: "usr", password: "pwd", readPreference: "some_invalid_read_preference" }, uris: ["127.0.0.1:27017"] } }; expect(() => { return getConnectionString(config2.db); }).to.throw("When specified, database config 'readPreference' " + "must be one of primary, primaryPreferred, secondary, secondaryPreferred, nearest"); }); it("should return a valid connection string that includes the specified replica set name", () => { const config2 = { db: { options: { database: "simple_dao_test", username: "usr", password: "pwd", replicaSet: "replica_set_name" }, uris: ["127.0.0.1:27017"] } }; const connectionString = getConnectionString(config2.db); expect(connectionString) .to.eql(`mongodb://usr:pwd@127.0.0.1:27017/simple_dao_test?authMechanism=DEFAULT&replicaSet=${config2.db.options.replicaSet}`); }); it("should return a valid connection string that includes the authentication source and if ssl", () => { const config2 = { db: { options: { database: "simple_dao_test", username: "usr", password: "pwd", replicaSet: "replica_set_name", authSource: "admin", ssl: true }, uris: [ "host1:1024", "host1:1025" ] } }; const connectionString = getConnectionString(config2.db); expect(connectionString) .to.eql("mongodb://usr:pwd@host1:1024,host1:1025/simple_dao_test?authMechanism=DEFAULT&replicaSet=replica_set_name&authSource=admin&ssl=true"); }); it("should return a valid connection string that includes the SRV record", () => { const config2 = { db: { options: { database: "simple_dao_test", username: "usr", password: "pwd", useSRVRecord: true }, uris: ["127.0.0.1:27017"] } }; const connectionString = getConnectionString(config2.db); expect(connectionString) .to.eql("mongodb+srv://usr:pwd@127.0.0.1:27017/simple_dao_test?authMechanism=DEFAULT"); }); }); describe(".connect()", () => { let configForOtherDatabase = null; beforeEach(() => { configForOtherDatabase = { db: { options: { database: "simple_dao_test_2", username: "", password: "" }, uris: ["127.0.0.1:27017"] } }; }); it("should connect to the database and return an object that allows operations on the specified database", async () => { console.log(simpleDao.connectionString); const db = await simpleDao.connect(); expect(db.databaseName).to.eql(config.db.options.database); const result = await db.collection("test_collection").insertOne({test: true}); const _id = result.insertedId; const [insertedDocument] = await db.collection("test_collection").find({_id}).toArray(); expect(insertedDocument).to.contain({test: true}); }); it("should share database connections across multiple instances of the SimpleDao " + "when a connection to a particular database has already been established", async () => { const connectionSpy = sandbox.spy(MongoClient, "connect"); expect(connectionSpy.callCount).to.eql(0); const simpleDao2 = new SimpleDao(configForOtherDatabase); const db2 = await simpleDao2.connect(); expect(connectionSpy.callCount).to.eql(1); // Create a new instance of the SimpleDao and connect to the database again. // Since we already connected to this database in the previous instance, we expect that connection to be re-used. const simpleDao3 = new SimpleDao(configForOtherDatabase); const db3 = await simpleDao3.connect(); expect(db3 === db2).to.be.true; expect(connectionSpy.callCount).to.eql(1); // Change which database we are connecting to configForOtherDatabase.db.options.database = "simple_dao_test_3"; // Create a new instance. We expect it to form a new connection, since we haven't connected to this database yet. const simpleDao4 = new SimpleDao(configForOtherDatabase); const db4 = await simpleDao4.connect(); expect(db4 === db3).to.be.false; expect(connectionSpy.callCount).to.eql(2); // Create another instance, which should re-use the connection from the previous instance const simpleDao5 = new SimpleDao(configForOtherDatabase); const db5 = await simpleDao5.connect(); expect(db5 === db4).to.be.true; expect(connectionSpy.callCount).to.eql(2); }); it("should automatically reconnect when the database connection was unexpectedly closed", async () => { // Change which database we are connecting to configForOtherDatabase.db.options.database = "simple_dao_test_4"; const connectionSpy = sandbox.spy(MongoClient, "connect"); expect(connectionSpy.callCount).to.eql(0); const simpleDao2 = new SimpleDao(configForOtherDatabase); const dbConnection2 = await simpleDao2.connect(); expect(connectionSpy.callCount).to.eql(1); // Close the database connection. // The next time we try to connect, we expect the simpleDao to form a new connection to the database. const client = await simpleDao2._getMongoClient(); await client.close(); const dbConnection3 = await simpleDao2.connect(); expect(dbConnection2 === dbConnection3).to.be.false; expect(connectionSpy.callCount).to.eql(2); }); it("should reconnect on subsequent calls after the initial connection rejects with an error", async () => { // Change which database we are connecting to configForOtherDatabase.db.options.database = "simple_dao_test_5"; const connectionStub = sandbox.stub(MongoClient, "connect").rejects(new Error("Some mongo error")); expect(connectionStub.callCount).to.eql(0); const simpleDao2 = new SimpleDao(configForOtherDatabase); try { await simpleDao2.connect(); expect.fail(); } catch (err) { expect(err.message).to.eql("Some mongo error"); expect(connectionStub.callCount).to.eql(1); } // Allow the database connection to proceed normally, without rejection. // We expect the simpleDao to form a new connection to the database. connectionStub.reset(); expect(connectionStub.callCount).to.eql(0); connectionStub.callThrough(); await simpleDao2.connect(); expect(connectionStub.callCount).to.eql(1); }); it("should connect to the database only once when multiple database requests arrive while the initial connection is still being " + "established", async () => { // Change which database we are connecting to configForOtherDatabase.db.options.database = "simple_dao_test_6"; const simpleDao2 = new SimpleDao(configForOtherDatabase); const connectionSpy = sandbox.spy(MongoClient, "connect"); expect(connectionSpy.callCount).to.eql(0); await Promise.all([ simpleDao2.for(Model).find({}), simpleDao2.for(Model).find({}), simpleDao2.for(Model).find({}) ]); expect(connectionSpy.callCount).to.eql(1); }); }); // this exists for compatibility with the soon-to-be-removed mongoskin API describe("connect().then(db => db.gridfs)", () => { let db = null; const GridStore = require("mongodb").GridStore; beforeEach(() => { return simpleDao.connect().then((database) => { db = database; }); }); it("should allow writing files", () => { const fileName = "tintin"; const path = "test/fixtures/tintin.jpg"; const data = require("fs").readFileSync(path); return new Promise((resolve, reject) => { db.gridfs().open(fileName, "w", (err, gs) => { if (err) { reject(err, null); return; } gs.write(data, (err2) => { if (err2) { reject(err2, null); return; } gs.close(resolve); }); }); }).then(() => { const gs = new GridStore(db, fileName, "r"); gs.open((_err, _gsx) => { gs.seek(0, () => { gs.read((_err2, readData) => { expect(data.toString("base64")).to.eq(readData.toString("base64")); }); }); }); }); }); it("should allow reading files", (done) => { const fileName = "tintin"; const path = "test/fixtures/tintin.jpg"; const data = require("fs").readFileSync(path); const gridStore = new GridStore(db, fileName, "w"); gridStore.open((_err, gridStore1) => { gridStore1.write(data, (_err2, gridStore2) => { gridStore2.close((_err3, _result) => { return simpleDao.connect().then((db2) => { db2.gridfs().open(fileName, "r", (_err4, gs) => { gs.read((_err5, readData) => { expect(data.toString("base64")).to.eq(readData.toString("base64")); done(); }); }); }); }); }); }); }); }); describe(".collectionNames()", () => { let db = null; beforeEach(async () => { db = await simpleDao.connect(); await db.dropDatabase(); }); it("should return an empty array if there are no collections in the database", async () => { const collectionNames = await simpleDao.collectionNames(); expect(collectionNames).to.eql([]); }); it("should return a list of all collection names in the database", async () => { await db.collection("collection_1").insert({}); await db.collection("collection_2").insert({}); const collectionNames = await simpleDao.collectionNames(); expect(collectionNames).to.be.an("array").that.includes.members(["collection_1", "collection_2"]); }); }); describe(".dropCollection()", () => { it("should drop the specified collection", async () => { const db = await simpleDao.connect(); let collectionExists = await databaseHasCollection(db, collectionName); expect(collectionExists).to.be.false; await simpleDao.save(model); collectionExists = await databaseHasCollection(db, collectionName); expect(collectionExists).to.be.true; await simpleDao.dropCollection(collectionName); collectionExists = await databaseHasCollection(db, collectionName); expect(collectionExists).to.be.false; }); }); describe(".for()", () => { it("should return an Operator with the correct properties", () => { const operator = simpleDao.for(Model); expect(operator.simpleDao).to.eql(simpleDao); expect(operator.collectionName).to.eql(Model.collectionName()); expect(operator.factory).to.eql(Model.factory); }); it("should throw an error if the provided constructor function does not have a 'factory' method", () => { expect(() => { simpleDao.for({}); }).to.throw("SimpleDao: The provided constructor function or class needs to have a factory function"); }); }); describe(".aggregate()", () => { it("should perform the specified aggregate query on the specified collection", async () => { const modelOne = Model.factory({a: 1}); const modelTwo = Model.factory({a: 2}); await Promise.all([ simpleDao.save(modelOne), simpleDao.save(modelTwo) ]); const query = {$group: {_id: 1, total: {$sum: "$a"}}}; const cursor = await simpleDao.aggregate(collectionName, query); expect(cursor.constructor.name).to.eql("AggregationCursor"); const result = await cursor.toArray(); expect(result).to.deep.eql([{_id: 1, total: 3}]); }); it("should reject if an error was encountered when connecting to the database", async () => { sandbox.stub(simpleDao, "connect").rejects(new Error("Some connection error")); try { await simpleDao.aggregate(collectionName, {}); expect(false).to.be.true; } catch (err) { expect(err.message).to.eql("Some connection error"); } }); }); describe(".save()", () => { it("should save the model to the correct collection, as defined by the model constructor's `collectionName()` function", async () => { const db = await simpleDao.connect(); let allDocumentsInCollection = await db.collection(model.constructor.collectionName()).find({}).toArray(); expect(allDocumentsInCollection).to.have.length(0); await simpleDao.save(model); allDocumentsInCollection = await db.collection(model.constructor.collectionName()).find({}).toArray(); expect(allDocumentsInCollection).to.have.length(1); expect(allDocumentsInCollection[0]._id.toString()).to.eql(model._id.toString()); }); it("should return the model", async () => { const result = await simpleDao.save(model); Reflect.deleteProperty(result, "_id"); expect(result).to.deep.eql(model); }); it("should reject if a model is not provided", async () => { try { await simpleDao.save(); expect(false).to.be.true; } catch (err) { expect(err.message).to.eql("SimpleDao: No data was provided in the call to .save()"); } }); it("should mutate the model and assign the _id from the saved db document when the original model doesn't have an _id", async () => { expect(model._id).to.not.exist; await simpleDao.save(model); expect(model._id).to.exist; expect(model._id).to.be.an.instanceOf(ObjectID); }); it("should save the model with its existing _id when the model is provided with an _id", async () => { const _id = ObjectID(); model._id = _id; await simpleDao.save(model); expect(model._id.toString()).to.eql(_id.toString()); }); it("should mutate the model and set the value of 'model.updatedAt.value' to the current date, " + "when the model has an 'updatedAt.value' property", async () => { expect(model.updatedAt).to.not.exist; await simpleDao.save(model); expect(model.updatedAt).to.not.exist; model.updatedAt = {}; await simpleDao.save(model); expect(model.updatedAt.value).to.not.exist; model.updatedAt = {value: "some value"}; await simpleDao.save(model); expect(model.updatedAt.value).to.exist; expect(model.updatedAt.value).to.be.an.instanceOf(Date); // Check that the updatedAt timestamp is within 10 seconds of now const currentTimestamp = new Date().getTime(); expect(model.updatedAt.value.getTime()).to.be.within(currentTimestamp - 10000, currentTimestamp + 10000); }); }); describe("Operator methods", () => { let modelOne = null; let modelTwo = null; let modelThree = null; beforeEach(async () => { modelOne = Model.factory({a: 1}); modelTwo = Model.factory({a: 2}); modelThree = Model.factory({a: 2}); await Promise.all([ simpleDao.save(modelOne), simpleDao.save(modelTwo), simpleDao.save(modelThree) ]); }); describe(".count()", () => { it("should return the number of records that match the specified query", async () => { let count = await simpleDao.for(Model).count({a: 1}); expect(count).to.eql(1); count = await simpleDao.for(Model).count({a: 2}); expect(count).to.eql(2); }); it("should reject if an error was encountered when connecting to the database", async () => { sandbox.stub(simpleDao, "connect").rejects(new Error("Some connection error")); try { await simpleDao.for(Model).count({}); expect(false).to.be.true; } catch (err) { expect(err.message).to.eql("Some connection error"); } }); }); describe(".find()", () => { describe(".toArray()", () => { it("should return an array of all documents that match the specified query", async () => { let results = await simpleDao.for(Model).find({a: {$gt: 0}}).toArray(); expect(results).to.have.length(3); results = await simpleDao.for(Model).find({a: 1}).toArray(); expect(results).to.have.length(1); }); it("should return an array of objects that are instances of the provided class, " + "created via the class' .factory() method", async () => { const factorySpy = sandbox.spy(Model, "factory"); expect(factorySpy.callCount).to.eql(0); const results = await simpleDao.for(Model).find({}).toArray(); expect(results).to.have.length.gt(0); expect(factorySpy.callCount).to.eql(results.length); for (const data of results) { expect(data).to.be.an.instanceOf(Model); } }); it("should reject if there was an error performing the query", async () => { try { await simpleDao.for(Model).find({a: {$badOperator: 0}}).toArray(); expect(false).to.be.true; } catch (err) { expect(err.message).to.eql("unknown operator: $badOperator"); } }); }); describe(".toCursor()", () => { it("should return a cursor for all documents that match the specified query", async () => { const cursor = await simpleDao.for(Model).find({a: {$gt: 0}}).toCursor(); expect(cursor).to.be.an.instanceOf(Cursor); const results = await cursor.toArray(); expect(results).to.have.length(3); }); }); }); describe(".findOne()", () => { it("should return only one object that matches the specified query", async () => { const result = await simpleDao.for(Model).findOne({a: 2}); expect(result).to.exist; expect(result.a).to.eql(2); }); it("should return null if there is no document matching the specified query", async () => { const result = await simpleDao.for(Model).findOne({a: 3}); expect(result).to.eql(null); }); it("should return an object that is an instance of the provided class, created via the class' .factory() method", async () => { const factorySpy = sandbox.spy(Model, "factory"); expect(factorySpy.callCount).to.eql(0); const result = await simpleDao.for(Model).findOne({}); expect(result).to.exist; expect(result).to.be.an.instanceOf(Model); expect(factorySpy.callCount).to.eql(1); }); it("should reject if there was an error performing the query", async () => { try { await simpleDao.for(Model).findOne({a: {$badOperator: 0}}); expect(true).to.eql(false); } catch (err) { expect(err.message).to.contain("unknown operator"); } }); }); describe(".findById()", () => { context("when the provided 'id' is an Object ID", () => { it("should return the single object that has the specified id", async () => { const result = await simpleDao.for(Model).findById(modelOne._id); expect(result).to.exist; expect(result._id.toString()).to.eql(modelOne._id.toString()); }); }); context("when the provided 'id' is a string", () => { it("should return the single object that has the specified id", async () => { const result = await simpleDao.for(Model).findById(modelOne._id.toString()); expect(result).to.exist; expect(result._id.toString()).to.eql(modelOne._id.toString()); }); it("should reject if the provided string is not a valid Object ID", async () => { try { await simpleDao.for(Model).findById("1"); expect(true).to.eql(false); } catch (err) { expect(err.message).to.eql("Argument passed in must be a single String of 12 bytes or a string of 24 hex characters"); } }); }); it("should return an object that is an instance of the provided class, created via the class' .factory() method", async () => { const factorySpy = sandbox.spy(Model, "factory"); expect(factorySpy.callCount).to.eql(0); const result = await simpleDao.for(Model).findById(modelOne._id); expect(result).to.exist; expect(result).to.be.an.instanceOf(Model); expect(factorySpy.callCount).to.eql(1); }); it("should return null if there is no document with the specified id", async () => { const result = await simpleDao.for(Model).findById(new ObjectID()); expect(result).to.eql(null); }); }); describe(".findAggregate()", () => { describe(".toArray()", () => { it("should return an array of all documents produced by the specified aggregate query", async () => { const query = {$group: {_id: 1, total: {$sum: "$a"}}}; const result = await simpleDao.for(Model).findAggregate(query).toArray(); expect(result).to.deep.eql([{_id: 1, total: 5}]); }); it("should return an array of objects that are instances of the provided class, " + "created via the class' .factory() method", async () => { const factorySpy = sandbox.spy(Model, "factory"); expect(factorySpy.callCount).to.eql(0); const results = await simpleDao.for(Model).findAggregate({$group: {_id: 1, total: {$sum: "$a"}}}).toArray(); expect(results).to.have.length.gt(0); expect(factorySpy.callCount).to.eql(results.length); for (const data of results) { expect(data).to.be.an.instanceOf(Model); } }); it("should reject if there was an error performing the query", async () => { try { await simpleDao.for(Model).findAggregate({a: {$badOperator: 0}}).toArray(); expect(true).to.eql(false); } catch (err) { expect(err.message).to.eql("Unrecognized pipeline stage name: 'a'"); } }); }); describe(".toCursor()", () => { it("should return a cursor for all documents produced by the specified aggregate query", async () => { const cursor = await simpleDao.for(Model).findAggregate({$match: {a: {$gt: 0}}}).toCursor(); expect(cursor.constructor.name).to.eql("AggregationCursor"); const results = await cursor.toArray(); expect(results).to.have.length(3); }); }); }); describe(".update()", () => { it("should reject if no query is provided", async () => { try { await simpleDao.for(Model).update(); expect(true).to.eql(false); } catch (err) { expect(err.message).to.eql("query can't be undefined or null"); } }); it("should reject if no update parameter is provided", async () => { try { await simpleDao.for(Model).update({}); expect(true).to.eql(false); } catch (err) { expect(err.message).to.eql("update can't be undefined or null"); } }); it("should update only one document by default", async () => { const result = await simpleDao.for(Model).update({}, {$set: {a: 5}}); expect(result).to.deep.eql({n: 1, nModified: 1, ok: 1, updatedExisting: true}); }); it("should update multiple documents when the `multi: true` option is provided", async () => { const result = await simpleDao.for(Model).update({}, {$set: {a: 5}}, {multi: true}); expect(result).to.deep.eql({n: 3, nModified: 3, ok: 1, updatedExisting: true}); }); it("should not update anything if the provided query matches no documents", async () => { const result = await simpleDao.for(Model).update({b: 1}, {$set: {a: 5}}); expect(result).to.deep.eql({n: 0, nModified: 0, ok: 1, updatedExisting: false}); }); it("should reject if the update operation is invalid", async () => { try { await simpleDao.for(Model).update({b: 1}, {$badOperator: {a: 5}}); expect(true).to.eql(false); } catch (err) { expect(err.message).to.eql("Unknown modifier: $badOperator. Expected a valid update modifier or pipeline-style update specified as an array"); } }); it("should reject if an error was encountered when connecting to the database", async () => { sandbox.stub(simpleDao, "connect").rejects(new Error("Some connection error")); try { await simpleDao.for(Model).update({}, {$set: {a: 5}}); expect(false).to.eql(true); } catch (err) { expect(err.message).to.eql("Some connection error"); } }); }); describe(".remove()", () => { it("should remove all documents that match the provided query", async () => { const db = await simpleDao.connect(); const query = {a: 2}; const documentsPriorToRemoval = await db.collection(collectionName).find(query).toArray(); expect(documentsPriorToRemoval).to.have.length(2); const result = await simpleDao.for(Model).remove(query); expect(result).to.deep.eql({n: 2, ok: 1}); const documentsAfterRemoval = await db.collection(collectionName).find(query).toArray(); expect(documentsAfterRemoval.length).to.eql(0); }); it("should remove no documents if the provided query matches no documents", async () => { const db = await simpleDao.connect(); const allDocumentsInCollectionPriorToRemoval = await db.collection(collectionName).find({}).toArray(); expect(allDocumentsInCollectionPriorToRemoval).to.have.length(3); const query = {a: 5}; const result = await simpleDao.for(Model).remove(query); expect(result).to.deep.eql({n: 0, ok: 1}); const allDocumentsInCollectionAfterRemoval = await db.collection(collectionName).find({}).toArray(); expect(allDocumentsInCollectionAfterRemoval.length).to.eql(3); }); it("should reject if no query is provided", async () => { try { await simpleDao.for(Model).remove(); expect(true).to.eql(false); } catch (err) { expect(err.message).to.eql("query can't be undefined or null"); } }); it("should reject if the query is invalid", async () => { try { await simpleDao.for(Model).remove({$badOperator: 1}); expect(true).to.eql(false); } catch (err) { expect(err.message).to.eql("unknown top level operator: $badOperator"); } }); it("should reject if an error was encountered when connecting to the database", async () => { sandbox.stub(simpleDao, "connect").rejects(new Error("Some connection error")); try { await simpleDao.for(Model).remove({}); expect(false).to.eql(true); } catch (err) { expect(err.message).to.eql("Some connection error"); } }); }); describe(".removeById()", () => { context("when the provided 'id' is an Object ID", () => { it("should remove the single document that has the specified id", async () => { const result = await simpleDao.for(Model).removeById(modelOne._id); expect(result).to.deep.eql({n: 1, ok: 1}); await expectDocumentDoesNotExist(modelOne._id); }); }); context("when the provided 'id' is a string", () => { it("should remove the single document that has the specified id", async () => { const result = await simpleDao.for(Model).removeById(modelOne._id.toString()); expect(result).to.deep.eql({n: 1, ok: 1}); await expectDocumentDoesNotExist(modelOne._id); }); it("should reject if the provided string is not a valid Object ID", async () => { try { await simpleDao.for(Model).removeById("1"); expect(true).to.be.false; } catch (err) { expect(err.message).to.eql("Argument passed in must be a single String of 12 bytes or a string of 24 hex characters"); } }); }); it("should do nothing if there is no document with the specified id", async () => { const result = await simpleDao.for(Model).removeById(new ObjectID()); expect(result).to.deep.eql({n: 0, ok: 1}); }); }); describe(".distinct()", () => { it("should return an empty array when no field is provided", async () => { const results = await simpleDao.for(Model).distinct(); expect(results).to.deep.eql([]); }); it("should return all distinct values for the provided field when no query is specified", async () => { const results = await simpleDao.for(Model).distinct("a"); expect(results).to.be.an("array").that.includes.members([1, 2]); }); it("should return the distinct values for the provided field amongst all documents that match the provided query", async () => { const modelFour = Model.factory({a: 3}); await simpleDao.save(modelFour); const results = await simpleDao.for(Model).distinct("a", {a: {$gt: 1}}); expect(results).to.be.an("array").that.includes.members([2, 3]); }); it("should reject if the query is invalid", async () => { try { await simpleDao.for(Model).distinct("a", {$badOperator: 1}); expect(false).to.be.true; } catch (err) { expect(err.message).to.eql("unknown top level operator: $badOperator"); } }); it("should reject if an error was encountered when connecting to the database", async () => { sandbox.stub(simpleDao, "connect").rejects(new Error("Some connection error")); try { await simpleDao.for(Model).distinct("a"); expect(1).to.equal(0); } catch (e) { expect(e.message).to.eql("Some connection error"); } }); }); }); });