UNPKG

btrz-simple-dao

Version:
1,047 lines (909 loc) 39 kB
const { describe, it, beforeEach, afterEach } = require("node:test"); const assert = require("node:assert").strict; const { deepEqual } = require("node:assert"); // eslint-disable-next-line max-statements describe("SimpleDao", () => { const Chance = require("chance"); const chance = new Chance(); const { ObjectID, MongoClient, Cursor } = require("mongodb"); 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}); assert.ok(document == null); } 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", () => { assert.ok(SimpleDao.objectId() instanceof ObjectID); }); it("should return an objectId from the given 24 characters argument", () => { const id = "55b27c2a74757b3c5e121b0e"; deepEqual(SimpleDao.objectId(id).toString(), id); }); }); describe("instance method", () => { it("should return a new objectId", () => { assert.ok(simpleDao.objectId() instanceof ObjectID); }); it("should return an objectId from the given 24 characters argument", () => { const id = "55b27c2a74757b3c5e121b0e"; deepEqual(simpleDao.objectId(id).toString(), id); }); }); }); describe("getConnectionString()", () => { it("should return a valid connection string for one db server", () => { const connectionString = getConnectionString(config.db); deepEqual(connectionString, "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); deepEqual(connectionString, "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); deepEqual(connectionString, "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); deepEqual(connectionString, "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); deepEqual(connectionString, "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); deepEqual(connectionString, `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"] } }; assert.throws(() => getConnectionString(config2.db), { message: "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); deepEqual(connectionString, `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"] } }; assert.throws(() => getConnectionString(config2.db), { message: "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); deepEqual(connectionString, `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); deepEqual(connectionString, "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); deepEqual(connectionString, "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(); deepEqual(db.databaseName, 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(); assert.strictEqual(insertedDocument.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"); assert.strictEqual(connectionSpy.callCount, 0); const simpleDao2 = new SimpleDao(configForOtherDatabase); const db2 = await simpleDao2.connect(); assert.strictEqual(connectionSpy.callCount, 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(); assert.strictEqual(db3 === db2, true); assert.strictEqual(connectionSpy.callCount, 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(); assert.strictEqual(db4 === db3, false); assert.strictEqual(connectionSpy.callCount, 2); // Create another instance, which should re-use the connection from the previous instance const simpleDao5 = new SimpleDao(configForOtherDatabase); const db5 = await simpleDao5.connect(); assert.strictEqual(db5 === db4, true); assert.strictEqual(connectionSpy.callCount, 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"); assert.strictEqual(connectionSpy.callCount, 0); const simpleDao2 = new SimpleDao(configForOtherDatabase); const dbConnection2 = await simpleDao2.connect(); assert.strictEqual(connectionSpy.callCount, 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(); assert.strictEqual(dbConnection2 === dbConnection3, false); assert.strictEqual(connectionSpy.callCount, 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")); assert.strictEqual(connectionStub.callCount, 0); const simpleDao2 = new SimpleDao(configForOtherDatabase); try { await simpleDao2.connect(); assert.fail(); } catch (err) { assert.strictEqual(err.message, "Some mongo error"); assert.strictEqual(connectionStub.callCount, 1); } // Allow the database connection to proceed normally, without rejection. // We expect the simpleDao to form a new connection to the database. connectionStub.reset(); assert.strictEqual(connectionStub.callCount, 0); connectionStub.callThrough(); await simpleDao2.connect(); assert.strictEqual(connectionStub.callCount, 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"); assert.strictEqual(connectionSpy.callCount, 0); await Promise.all([ simpleDao2.for(Model).find({}), simpleDao2.for(Model).find({}), simpleDao2.for(Model).find({}) ]); assert.strictEqual(connectionSpy.callCount, 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); return; } gs.write(data, (err2) => { if (err2) { reject(err2); return; } gs.close((err3) => { if (err3) { reject(err3); return; } resolve(); }); }); }); }).then(() => new Promise((resolve, reject) => { const gs = new GridStore(db, fileName, "r"); gs.open((errOpen) => { if (errOpen) { reject(errOpen); return; } gs.seek(0, (errSeek) => { if (errSeek) { reject(errSeek); return; } gs.read((errRead, readData) => { if (errRead) { reject(errRead); return; } assert.strictEqual(data.toString("base64"), readData.toString("base64")); resolve(); }); }); }); })); }); it("should allow reading files", () => { const fileName = "tintin"; const path = "test/fixtures/tintin.jpg"; const data = require("fs").readFileSync(path); return new Promise((resolve, reject) => { const gridStore = new GridStore(db, fileName, "w"); gridStore.open((err, gridStore1) => { if (err) { reject(err); return; } gridStore1.write(data, (err2, gridStore2) => { if (err2) { reject(err2); return; } gridStore2.close((err3) => { if (err3) { reject(err3); return; } simpleDao.connect().then((db2) => { db2.gridfs().open(fileName, "r", (err4, gs) => { if (err4) { reject(err4); return; } gs.read((err5, readData) => { if (err5) { reject(err5); return; } assert.strictEqual(data.toString("base64"), readData.toString("base64")); resolve(); }); }); }).catch(reject); }); }); }); }); }); }); 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(); deepEqual(collectionNames, []); }); 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(); assert.ok(Array.isArray(collectionNames)); assert.ok(collectionNames.includes("collection_1")); assert.ok(collectionNames.includes("collection_2")); }); }); describe(".dropCollection()", () => { it("should drop the specified collection", async () => { const db = await simpleDao.connect(); let collectionExists = await databaseHasCollection(db, collectionName); assert.strictEqual(collectionExists, false); await simpleDao.save(model); collectionExists = await databaseHasCollection(db, collectionName); assert.strictEqual(collectionExists, true); await simpleDao.dropCollection(collectionName); collectionExists = await databaseHasCollection(db, collectionName); assert.strictEqual(collectionExists, false); }); }); describe(".for()", () => { it("should return an Operator with the correct properties", () => { const operator = simpleDao.for(Model); assert.strictEqual(operator.simpleDao, simpleDao); deepEqual(operator.collectionName, Model.collectionName()); assert.strictEqual(operator.factory, Model.factory); }); it("should throw an error if the provided constructor function does not have a 'factory' method", () => { assert.throws(() => simpleDao.for({}), { message: "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); assert.strictEqual(cursor.constructor.name, "AggregationCursor"); const result = await cursor.toArray(); deepEqual(result, [{_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, {}); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "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(); assert.strictEqual(allDocumentsInCollection.length, 0); await simpleDao.save(model); allDocumentsInCollection = await db.collection(model.constructor.collectionName()).find({}).toArray(); assert.strictEqual(allDocumentsInCollection.length, 1); assert.strictEqual(allDocumentsInCollection[0]._id.toString(), model._id.toString()); }); it("should return the model", async () => { const result = await simpleDao.save(model); Reflect.deleteProperty(result, "_id"); deepEqual(result, model); }); it("should reject if a model is not provided", async () => { try { await simpleDao.save(); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "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 () => { assert.ok(model._id == null); await simpleDao.save(model); assert.ok(model._id != null); assert.ok(model._id 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); assert.strictEqual(model._id.toString(), _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 () => { assert.ok(model.updatedAt == null); await simpleDao.save(model); assert.ok(model.updatedAt == null); model.updatedAt = {}; await simpleDao.save(model); assert.ok(model.updatedAt.value == null); model.updatedAt = {value: "some value"}; await simpleDao.save(model); assert.ok(model.updatedAt.value != null); assert.ok(model.updatedAt.value instanceof Date); // Check that the updatedAt timestamp is within 10 seconds of now const currentTimestamp = new Date().getTime(); assert.ok(model.updatedAt.value.getTime() >= currentTimestamp - 10000 && model.updatedAt.value.getTime() <= 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}); assert.strictEqual(count, 1); count = await simpleDao.for(Model).count({a: 2}); assert.strictEqual(count, 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({}); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "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(); assert.strictEqual(results.length, 3); results = await simpleDao.for(Model).find({a: 1}).toArray(); assert.strictEqual(results.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"); assert.strictEqual(factorySpy.callCount, 0); const results = await simpleDao.for(Model).find({}).toArray(); assert.ok(results.length > 0); assert.strictEqual(factorySpy.callCount, results.length); for (const data of results) { assert.ok(data instanceof Model); } }); it("should reject if there was an error performing the query", async () => { try { await simpleDao.for(Model).find({a: {$badOperator: 0}}).toArray(); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "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(); assert.ok(cursor instanceof Cursor); const results = await cursor.toArray(); assert.strictEqual(results.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}); assert.ok(result != null); assert.strictEqual(result.a, 2); }); it("should return null if there is no document matching the specified query", async () => { const result = await simpleDao.for(Model).findOne({a: 3}); assert.strictEqual(result, 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"); assert.strictEqual(factorySpy.callCount, 0); const result = await simpleDao.for(Model).findOne({}); assert.ok(result != null); assert.ok(result instanceof Model); assert.strictEqual(factorySpy.callCount, 1); }); it("should reject if there was an error performing the query", async () => { try { await simpleDao.for(Model).findOne({a: {$badOperator: 0}}); assert.fail("expected rejection"); } catch (err) { assert.ok(err.message.includes("unknown operator")); } }); }); describe(".findById()", () => { describe("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); assert.ok(result != null); assert.strictEqual(result._id.toString(), modelOne._id.toString()); }); }); describe("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()); assert.ok(result != null); assert.strictEqual(result._id.toString(), modelOne._id.toString()); }); it("should reject if the provided string is not a valid Object ID", async () => { try { await simpleDao.for(Model).findById("1"); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "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"); assert.strictEqual(factorySpy.callCount, 0); const result = await simpleDao.for(Model).findById(modelOne._id); assert.ok(result != null); assert.ok(result instanceof Model); assert.strictEqual(factorySpy.callCount, 1); }); it("should return null if there is no document with the specified id", async () => { const result = await simpleDao.for(Model).findById(new ObjectID()); assert.strictEqual(result, 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(); deepEqual(result, [{_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"); assert.strictEqual(factorySpy.callCount, 0); const results = await simpleDao.for(Model).findAggregate({$group: {_id: 1, total: {$sum: "$a"}}}).toArray(); assert.ok(results.length > 0); assert.strictEqual(factorySpy.callCount, results.length); for (const data of results) { assert.ok(data instanceof Model); } }); it("should reject if there was an error performing the query", async () => { try { await simpleDao.for(Model).findAggregate({a: {$badOperator: 0}}).toArray(); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "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(); assert.strictEqual(cursor.constructor.name, "AggregationCursor"); const results = await cursor.toArray(); assert.strictEqual(results.length, 3); }); }); }); describe(".update()", () => { it("should reject if no query is provided", async () => { try { await simpleDao.for(Model).update(); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "query can't be undefined or null"); } }); it("should reject if no update parameter is provided", async () => { try { await simpleDao.for(Model).update({}); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "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}}); deepEqual(result, {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}); deepEqual(result, {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}}); deepEqual(result, {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}}); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "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}}); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "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(); assert.strictEqual(documentsPriorToRemoval.length, 2); const result = await simpleDao.for(Model).remove(query); deepEqual(result, {n: 2, ok: 1}); const documentsAfterRemoval = await db.collection(collectionName).find(query).toArray(); assert.strictEqual(documentsAfterRemoval.length, 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(); assert.strictEqual(allDocumentsInCollectionPriorToRemoval.length, 3); const query = {a: 5}; const result = await simpleDao.for(Model).remove(query); deepEqual(result, {n: 0, ok: 1}); const allDocumentsInCollectionAfterRemoval = await db.collection(collectionName).find({}).toArray(); assert.strictEqual(allDocumentsInCollectionAfterRemoval.length, 3); }); it("should reject if no query is provided", async () => { try { await simpleDao.for(Model).remove(); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "query can't be undefined or null"); } }); it("should reject if the query is invalid", async () => { try { await simpleDao.for(Model).remove({$badOperator: 1}); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "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({}); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "Some connection error"); } }); }); describe(".removeById()", () => { describe("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); deepEqual(result, {n: 1, ok: 1}); await expectDocumentDoesNotExist(modelOne._id); }); }); describe("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()); deepEqual(result, {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"); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "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()); deepEqual(result, {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(); deepEqual(results, []); }); it("should return all distinct values for the provided field when no query is specified", async () => { const results = await simpleDao.for(Model).distinct("a"); assert.ok(Array.isArray(results)); assert.ok(results.includes(1)); assert.ok(results.includes(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}}); assert.ok(Array.isArray(results)); assert.ok(results.includes(2)); assert.ok(results.includes(3)); }); it("should reject if the query is invalid", async () => { try { await simpleDao.for(Model).distinct("a", {$badOperator: 1}); assert.fail("expected rejection"); } catch (err) { assert.strictEqual(err.message, "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"); assert.fail("expected rejection"); } catch (e) { assert.strictEqual(e.message, "Some connection error"); } }); }); }); });