UNPKG

34.2 kBJavaScriptView Raw
1const Chance = require("chance");
2const chance = new Chance();
3const {ObjectID, MongoClient, Cursor} = require("mongodb");
4const chai = require("chai");
5const expect = chai.expect;
6const chaiAsPromised = require("chai-as-promised");
7chai.use(chaiAsPromised);
8const sinon = require("sinon");
9const sandbox = sinon.createSandbox();
10const {ALL_AUTH_MECHANISMS, ALL_READ_PREFERENCES} = require("../constants");
11const SimpleDao = require("../").SimpleDao;
12const {getConnectionString} = require("../src/simple-dao");
13
14
15async function databaseHasCollection(db, collectionName) {
16 const allCollections = await db.listCollections().toArray();
17 return allCollections.some((collection) => { return collection.name === collectionName; });
18}
19
20
21describe("SimpleDao", () => {
22 let config = null;
23 let simpleDao = null;
24 let collectionName = null;
25 let model = null;
26
27 class Model {
28 static collectionName() {
29 return collectionName;
30 }
31
32 static factory(literal) {
33 return Object.assign(new Model(), literal);
34 }
35 }
36
37 async function expectDocumentDoesNotExist(id, _collectionName = collectionName) {
38 const db = await simpleDao.connect();
39 const document = await db.collection(_collectionName).findOne({_id: id});
40 expect(document).to.not.exist;
41 }
42
43 beforeEach(() => {
44 config = {
45 db: {
46 options: {
47 database: "simple_dao_test",
48 username: "",
49 password: ""
50 },
51 uris: ["127.0.0.1:27017"]
52 }
53 };
54 collectionName = chance.word({length: 10});
55 model = Model.factory({a: 1});
56
57 simpleDao = new SimpleDao(config);
58 });
59
60 afterEach(async () => {
61 sandbox.restore();
62
63 const db = await simpleDao.connect();
64 try {
65 await db.dropCollection(collectionName);
66 } catch (err) {}
67 });
68
69 describe(".objectId()", () => {
70 describe("static method", () => {
71 it("should return a new objectId", () => {
72 expect(SimpleDao.objectId()).to.be.an.instanceOf(ObjectID);
73 });
74
75 it("should return an objectId from the given 24 characters argument", () => {
76 const id = "55b27c2a74757b3c5e121b0e";
77 expect(SimpleDao.objectId(id).toString()).to.be.eql(id);
78 });
79 });
80
81 describe("instance method", () => {
82 it("should return a new objectId", () => {
83 expect(simpleDao.objectId()).to.be.an.instanceOf(ObjectID);
84 });
85
86 it("should return an objectId from the given 24 characters argument", () => {
87 const id = "55b27c2a74757b3c5e121b0e";
88 expect(simpleDao.objectId(id).toString()).to.be.eql(id);
89 });
90 });
91 });
92
93 describe("getConnectionString()", () => {
94 it("should return a valid connection string for one db server", () => {
95 const connectionString = getConnectionString(config.db);
96 expect(connectionString).to.eql("mongodb://127.0.0.1:27017/simple_dao_test?authMechanism=DEFAULT");
97 });
98
99 it("should return a valid connection string for one db server using authentication credentials", () => {
100 const config = {
101 db: {
102 options: {
103 database: "simple_dao_test",
104 username: "usr",
105 password: "pwd"
106 },
107 uris: ["127.0.0.1:27017"]
108 }
109 };
110 const connectionString = getConnectionString(config.db);
111 expect(connectionString).to.eql("mongodb://usr:pwd@127.0.0.1:27017/simple_dao_test?authMechanism=DEFAULT");
112 });
113
114 it("should URL-encode the authentication credentials " +
115 "so that credentials that include symbols will not result in invalid connection strings", () => {
116 const config = {
117 db: {
118 options: {
119 database: "simple_dao_test",
120 username: "u$ername",
121 password: "pa$$w{}rd"
122 },
123 uris: ["127.0.0.1:27017"]
124 }
125 };
126 const connectionString = getConnectionString(config.db);
127 expect(connectionString)
128 .to.eql("mongodb://u%24ername:pa%24%24w%7B%7Drd@127.0.0.1:27017/simple_dao_test?authMechanism=DEFAULT");
129 });
130
131 it("should return a valid connection string for many db servers using authentication credentials", () => {
132 const config = {
133 db: {
134 options: {
135 database: "simple_dao_test",
136 username: "usr",
137 password: "pwd"
138 },
139 uris: [
140 "127.0.0.1:27017",
141 "127.0.0.2:27018"
142 ]
143 }
144 };
145 const connectionString = getConnectionString(config.db);
146 expect(connectionString).to.eql("mongodb://usr:pwd@127.0.0.1:27017,127.0.0.2:27018/simple_dao_test?authMechanism=DEFAULT");
147 });
148
149 it("should return a valid connection string that includes the specified authentication mechanism", () => {
150 for (const authMechanism of ALL_AUTH_MECHANISMS) {
151 const config = {
152 db: {
153 options: {
154 database: "simple_dao_test",
155 username: "usr",
156 password: "pwd",
157 authMechanism
158 },
159 uris: ["127.0.0.1:27017"]
160 }
161 };
162 const connectionString = getConnectionString(config.db);
163 expect(connectionString).to.eql(`mongodb://usr:pwd@127.0.0.1:27017/simple_dao_test?authMechanism=${authMechanism}`);
164 }
165 });
166
167 it("should throw an error if an invalid authentication mechanism is specified", () => {
168 const config = {
169 db: {
170 options: {
171 database: "simple_dao_test",
172 username: "usr",
173 password: "pwd",
174 authMechanism: "some_invalid_auth_mechanism"
175 },
176 uris: ["127.0.0.1:27017"]
177 }
178 };
179 expect(() => { return getConnectionString(config.db); })
180 .to.throw("Database config 'authMechanism' must be one of DEFAULT, MONGODB-CR, SCRAM-SHA-1, SCRAM-SHA-256");
181 });
182
183 it("should return a valid connection string that includes the specified read preference", () => {
184 for (const readPreference of ALL_READ_PREFERENCES) {
185 const config = {
186 db: {
187 options: {
188 database: "simple_dao_test",
189 username: "usr",
190 password: "pwd",
191 readPreference
192 },
193 uris: ["127.0.0.1:27017"]
194 }
195 };
196 const connectionString = getConnectionString(config.db);
197 expect(connectionString)
198 .to.eql(`mongodb://usr:pwd@127.0.0.1:27017/simple_dao_test?authMechanism=DEFAULT&readPreference=${readPreference}`);
199 }
200 });
201
202 it("should throw an error if an invalid read preference is specified", () => {
203 const config = {
204 db: {
205 options: {
206 database: "simple_dao_test",
207 username: "usr",
208 password: "pwd",
209 readPreference: "some_invalid_read_preference"
210 },
211 uris: ["127.0.0.1:27017"]
212 }
213 };
214 expect(() => { return getConnectionString(config.db); }).to.throw("When specified, database config 'readPreference' " +
215 "must be one of primary, primaryPreferred, secondary, secondaryPreferred, nearest");
216 });
217
218 it("should return a valid connection string that includes the specified replica set name", () => {
219 const config = {
220 db: {
221 options: {
222 database: "simple_dao_test",
223 username: "usr",
224 password: "pwd",
225 replicaSet: "replica_set_name"
226 },
227 uris: ["127.0.0.1:27017"]
228 }
229 };
230 const connectionString = getConnectionString(config.db);
231 expect(connectionString)
232 .to.eql(`mongodb://usr:pwd@127.0.0.1:27017/simple_dao_test?authMechanism=DEFAULT&replicaSet=${config.db.options.replicaSet}`);
233 });
234 });
235
236 describe(".connect()", () => {
237 let configForOtherDatabase = null;
238
239 beforeEach(() => {
240 configForOtherDatabase = {
241 db: {
242 options: {
243 database: "simple_dao_test_2",
244 username: "",
245 password: ""
246 },
247 uris: ["127.0.0.1:27017"]
248 }
249 };
250 });
251
252 it("should connect to the database and return an object that allows operations on the specified database", async () => {
253 console.log(simpleDao.connectionString);
254 const db = await simpleDao.connect();
255 expect(db.databaseName).to.eql(config.db.options.database);
256 const result = await db.collection("test_collection").insertOne({test: true});
257 const _id = result.insertedId;
258 const [insertedDocument] = await db.collection("test_collection").find({_id}).toArray();
259 expect(insertedDocument).to.contain({test: true});
260 });
261
262 it("should share database connections across multiple instances of the SimpleDao " +
263 "when a connection to a particular database has already been established", async () => {
264 const connectionSpy = sandbox.spy(MongoClient, "connect");
265
266 expect(connectionSpy.callCount).to.eql(0);
267 const simpleDao2 = new SimpleDao(configForOtherDatabase);
268 const db2 = await simpleDao2.connect();
269 expect(connectionSpy.callCount).to.eql(1);
270
271 // Create a new instance of the SimpleDao and connect to the database again.
272 // Since we already connected to this database in the previous instance, we expect that connection to be re-used.
273 const simpleDao3 = new SimpleDao(configForOtherDatabase);
274 const db3 = await simpleDao3.connect();
275 expect(db3 === db2).to.be.true;
276 expect(connectionSpy.callCount).to.eql(1);
277
278 // Change which database we are connecting to
279 configForOtherDatabase.db.options.database = "simple_dao_test_3";
280
281 // Create a new instance. We expect it to form a new connection, since we haven't connected to this database yet.
282 const simpleDao4 = new SimpleDao(configForOtherDatabase);
283 const db4 = await simpleDao4.connect();
284 expect(db4 === db3).to.be.false;
285 expect(connectionSpy.callCount).to.eql(2);
286
287 // Create another instance, which should re-use the connection from the previous instance
288 const simpleDao5 = new SimpleDao(configForOtherDatabase);
289 const db5 = await simpleDao5.connect();
290 expect(db5 === db4).to.be.true;
291 expect(connectionSpy.callCount).to.eql(2);
292 });
293
294 it("should automatically reconnect when the database connection was unexpectedly closed", async () => {
295 // Change which database we are connecting to
296 configForOtherDatabase.db.options.database = "simple_dao_test_4";
297
298 const connectionSpy = sandbox.spy(MongoClient, "connect");
299
300 expect(connectionSpy.callCount).to.eql(0);
301 const simpleDao2 = new SimpleDao(configForOtherDatabase);
302 const dbConnection2 = await simpleDao2.connect();
303 expect(connectionSpy.callCount).to.eql(1);
304
305 // Close the database connection.
306 // The next time we try to connect, we expect the simpleDao to form a new connection to the database.
307 const client = await simpleDao2._getMongoClient();
308 await client.close();
309
310 const dbConnection3 = await simpleDao2.connect();
311 expect(dbConnection2 === dbConnection3).to.be.false;
312 expect(connectionSpy.callCount).to.eql(2);
313 });
314
315 it("should reconnect on subsequent calls after the initial connection rejects with an error", async () => {
316 // Change which database we are connecting to
317 configForOtherDatabase.db.options.database = "simple_dao_test_5";
318
319 const connectionStub = sandbox.stub(MongoClient, "connect").rejects(new Error("Some mongo error"));
320
321 expect(connectionStub.callCount).to.eql(0);
322 const simpleDao2 = new SimpleDao(configForOtherDatabase);
323 try {
324 await simpleDao2.connect();
325 expect.fail();
326 } catch (err) {
327 expect(err.message).to.eql("Some mongo error");
328 expect(connectionStub.callCount).to.eql(1);
329 }
330
331 // Allow the database connection to proceed normally, without rejection.
332 // We expect the simpleDao to form a new connection to the database.
333 connectionStub.reset();
334 expect(connectionStub.callCount).to.eql(0);
335 connectionStub.callThrough();
336 await simpleDao2.connect();
337 expect(connectionStub.callCount).to.eql(1);
338 });
339
340 it("should connect to the database only once when multiple database requests arrive while the initial connection is still being " +
341 "established", async () => {
342 // Change which database we are connecting to
343 configForOtherDatabase.db.options.database = "simple_dao_test_6";
344 const simpleDao2 = new SimpleDao(configForOtherDatabase);
345
346 const connectionSpy = sandbox.spy(MongoClient, "connect");
347 expect(connectionSpy.callCount).to.eql(0);
348
349 await Promise.all([
350 simpleDao2.for(Model).find({}),
351 simpleDao2.for(Model).find({}),
352 simpleDao2.for(Model).find({})
353 ]);
354
355 expect(connectionSpy.callCount).to.eql(1);
356 });
357 });
358
359 // this exists for compatibility with the soon-to-be-removed mongoskin API
360 describe("connect().then(db => db.gridfs)", () => {
361 let db = null;
362
363 const GridStore = require("mongodb").GridStore;
364
365 beforeEach(() => {
366 return simpleDao.connect().then((database) => {
367 db = database;
368 });
369 });
370
371 it("should allow writing files", (done) => {
372 const fileName = "tintin";
373 const path = "test/fixtures/tintin.jpg";
374 const data = require("fs").readFileSync(path);
375
376 return new Promise((resolve, reject) => {
377 db.gridfs().open(fileName, "w", (err, gs) => {
378 if (err) {
379 reject(err, null);
380 return;
381 }
382 gs.write(data, (err2) => {
383 if (err2) {
384 reject(err2, null);
385 return;
386 }
387 gs.close(resolve);
388 });
389 });
390 }).then(() => {
391 const gs = new GridStore(db, fileName, "r");
392 gs.open((err, gsx) => {
393 gs.seek(0, () => {
394 gs.read((err, readData) => {
395 expect(data.toString("base64")).to.eq(readData.toString("base64"));
396 done();
397 });
398 });
399 });
400 });
401 });
402
403 it("should allow reading files", (done) => {
404 const fileName = "tintin";
405 const path = "test/fixtures/tintin.jpg";
406 const data = require("fs").readFileSync(path);
407
408 const gridStore = new GridStore(db, fileName, "w");
409 gridStore.open((err, gridStore) => {
410 gridStore.write(data, (err, gridStore) => {
411 gridStore.close((err, result) => {
412 return simpleDao.connect().then((db) => {
413 db.gridfs().open(fileName, "r", (err, gs) => {
414 gs.read((err, readData) => {
415 expect(data.toString("base64")).to.eq(readData.toString("base64"));
416 done();
417 });
418 });
419 });
420 });
421 });
422 });
423 });
424 });
425
426 describe(".collectionNames()", () => {
427 let db = null;
428
429 beforeEach(async () => {
430 db = await simpleDao.connect();
431 await db.dropDatabase();
432 });
433
434 it("should return an empty array if there are no collections in the database", async () => {
435 const collectionNames = await simpleDao.collectionNames();
436 expect(collectionNames).to.eql([]);
437 });
438
439 it("should return a list of all collection names in the database", async () => {
440 await db.collection("collection_1").insert({});
441 await db.collection("collection_2").insert({});
442 const collectionNames = await simpleDao.collectionNames();
443 expect(collectionNames).to.be.an("array").that.includes.members(["collection_1", "collection_2"]);
444 });
445 });
446
447 describe(".dropCollection()", () => {
448 it("should drop the specified collection", async () => {
449 const db = await simpleDao.connect();
450 let collectionExists = await databaseHasCollection(db, collectionName);
451 expect(collectionExists).to.be.false;
452
453 await simpleDao.save(model);
454 collectionExists = await databaseHasCollection(db, collectionName);
455 expect(collectionExists).to.be.true;
456
457 await simpleDao.dropCollection(collectionName);
458 collectionExists = await databaseHasCollection(db, collectionName);
459 expect(collectionExists).to.be.false;
460 });
461 });
462
463 describe(".for()", () => {
464 it("should return an Operator with the correct properties", () => {
465 const operator = simpleDao.for(Model);
466 expect(operator.simpleDao).to.eql(simpleDao);
467 expect(operator.collectionName).to.eql(Model.collectionName());
468 expect(operator.factory).to.eql(Model.factory);
469 });
470
471 it("should throw an error if the provided constructor function does not have a 'factory' method", () => {
472 expect(() => simpleDao.for({})).to.throw("SimpleDao: The provided constructor function or class needs to have a factory function");
473 });
474 });
475
476 describe(".aggregate()", () => {
477 it("should perform the specified aggregate query on the specified collection", async () => {
478 const modelOne = Model.factory({a: 1});
479 const modelTwo = Model.factory({a: 2});
480 await Promise.all([
481 simpleDao.save(modelOne),
482 simpleDao.save(modelTwo)
483 ]);
484
485 const query = {$group: {_id: 1, total: {$sum: "$a"}}};
486 const cursor = await simpleDao.aggregate(collectionName, query);
487 expect(cursor.constructor.name).to.eql("AggregationCursor");
488 const result = await cursor.toArray();
489 expect(result).to.deep.eql([{_id: 1, total: 3}]);
490 });
491
492 it("should reject if an error was encountered when connecting to the database", async () => {
493 sandbox.stub(simpleDao, "connect").rejects(new Error("Some connection error"));
494 await expect(simpleDao.aggregate(collectionName, {})).to.eventually.be.rejectedWith("Some connection error");
495 });
496 });
497
498 describe(".save()", () => {
499 it("should save the model to the correct collection, as defined by the model constructor's `collectionName()` function", async () => {
500 const db = await simpleDao.connect();
501 let allDocumentsInCollection = await db.collection(model.constructor.collectionName()).find({}).toArray();
502 expect(allDocumentsInCollection).to.have.length(0);
503
504 await simpleDao.save(model);
505 allDocumentsInCollection = await db.collection(model.constructor.collectionName()).find({}).toArray();
506 expect(allDocumentsInCollection).to.have.length(1);
507 expect(allDocumentsInCollection[0]._id.toString()).to.eql(model._id.toString());
508 });
509
510 it("should return the model", async () => {
511 const result = await simpleDao.save(model);
512 Reflect.deleteProperty(result, "_id");
513 expect(result).to.deep.eql(model);
514 });
515
516 it("should reject if a model is not provided", async () => {
517 await expect(simpleDao.save()).to.eventually.be.rejectedWith("SimpleDao: No data was provided in the call to .save()");
518 });
519
520 it("should mutate the model and assign the _id from the saved db document when the original model doesn't have an _id", async () => {
521 expect(model._id).to.not.exist;
522 await simpleDao.save(model);
523 expect(model._id).to.exist;
524 expect(model._id).to.be.an.instanceOf(ObjectID);
525 });
526
527 it("should save the model with its existing _id when the model is provided with an _id", async () => {
528 const _id = ObjectID();
529 model._id = _id;
530 await simpleDao.save(model);
531 expect(model._id.toString()).to.eql(_id.toString());
532 });
533
534 it("should mutate the model and set the value of 'model.updatedAt.value' to the current date, " +
535 "when the model has an 'updatedAt.value' property", async () => {
536 expect(model.updatedAt).to.not.exist;
537
538 await simpleDao.save(model);
539 expect(model.updatedAt).to.not.exist;
540
541 model.updatedAt = {};
542 await simpleDao.save(model);
543 expect(model.updatedAt.value).to.not.exist;
544
545 model.updatedAt = {value: "some value"};
546 await simpleDao.save(model);
547 expect(model.updatedAt.value).to.exist;
548 expect(model.updatedAt.value).to.be.an.instanceOf(Date);
549 // Check that the updatedAt timestamp is within 10 seconds of now
550 const currentTimestamp = new Date().getTime();
551 expect(model.updatedAt.value.getTime()).to.be.within(currentTimestamp - 10000, currentTimestamp + 10000);
552 });
553 });
554
555 describe("Operator methods", () => {
556 let modelOne = null;
557 let modelTwo = null;
558 let modelThree = null;
559
560 beforeEach(async () => {
561 modelOne = Model.factory({a: 1});
562 modelTwo = Model.factory({a: 2});
563 modelThree = Model.factory({a: 2});
564
565 await Promise.all([
566 simpleDao.save(modelOne),
567 simpleDao.save(modelTwo),
568 simpleDao.save(modelThree)
569 ]);
570 });
571
572 describe(".count()", () => {
573 it("should return the number of records that match the specified query", async () => {
574 let count = await simpleDao.for(Model).count({a: 1});
575 expect(count).to.eql(1);
576
577 count = await simpleDao.for(Model).count({a: 2});
578 expect(count).to.eql(2);
579 });
580
581 it("should reject if an error was encountered when connecting to the database", async () => {
582 sandbox.stub(simpleDao, "connect").rejects(new Error("Some connection error"));
583 await expect(simpleDao.for(Model).count({})).to.eventually.be.rejectedWith("Some connection error");
584 });
585 });
586
587 describe(".find()", () => {
588 describe(".toArray()", () => {
589 it("should return an array of all documents that match the specified query", async () => {
590 let results = await simpleDao.for(Model).find({a: {$gt: 0}}).toArray();
591 expect(results).to.have.length(3);
592
593 results = await simpleDao.for(Model).find({a: 1}).toArray();
594 expect(results).to.have.length(1);
595 });
596
597 it("should return an array of objects that are instances of the provided class, " +
598 "created via the class' .factory() method", async () => {
599 const factorySpy = sandbox.spy(Model, "factory");
600 expect(factorySpy.callCount).to.eql(0);
601
602 const results = await simpleDao.for(Model).find({}).toArray();
603 expect(results).to.have.length.gt(0);
604 expect(factorySpy.callCount).to.eql(results.length);
605
606 for (const data of results) {
607 expect(data).to.be.an.instanceOf(Model);
608 }
609 });
610
611 it("should reject if there was an error performing the query", async () => {
612 return expect(simpleDao.for(Model).find({a: {$badOperator: 0}}).toArray()).to.eventually.be.rejectedWith("unknown operator");
613 });
614 });
615
616 describe(".toCursor()", () => {
617 it("should return a cursor for all documents that match the specified query", async () => {
618 const cursor = await simpleDao.for(Model).find({a: {$gt: 0}}).toCursor();
619 expect(cursor).to.be.an.instanceOf(Cursor);
620
621 const results = await cursor.toArray();
622 expect(results).to.have.length(3);
623 });
624 });
625 });
626
627 describe(".findOne()", () => {
628 it("should return only one object that matches the specified query", async () => {
629 const result = await simpleDao.for(Model).findOne({a: 2});
630 expect(result).to.exist;
631 expect(result.a).to.eql(2);
632 });
633
634 it("should return null if there is no document matching the specified query", async () => {
635 const result = await simpleDao.for(Model).findOne({a: 3});
636 expect(result).to.eql(null);
637 });
638
639 it("should return an object that is an instance of the provided class, created via the class' .factory() method", async () => {
640 const factorySpy = sandbox.spy(Model, "factory");
641 expect(factorySpy.callCount).to.eql(0);
642
643 const result = await simpleDao.for(Model).findOne({});
644 expect(result).to.exist;
645 expect(result).to.be.an.instanceOf(Model);
646 expect(factorySpy.callCount).to.eql(1);
647 });
648
649 it("should reject if there was an error performing the query", async () => {
650 return expect(simpleDao.for(Model).findOne({a: {$badOperator: 0}})).to.eventually.be.rejectedWith("unknown operator");
651 });
652 });
653
654 describe(".findById()", () => {
655 context("when the provided 'id' is an Object ID", () => {
656 it("should return the single object that has the specified id", async () => {
657 const result = await simpleDao.for(Model).findById(modelOne._id);
658 expect(result).to.exist;
659 expect(result._id.toString()).to.eql(modelOne._id.toString());
660 });
661 });
662
663 context("when the provided 'id' is a string", () => {
664 it("should return the single object that has the specified id", async () => {
665 const result = await simpleDao.for(Model).findById(modelOne._id.toString());
666 expect(result).to.exist;
667 expect(result._id.toString()).to.eql(modelOne._id.toString());
668 });
669
670 it("should reject if the provided string is not a valid Object ID", async () => {
671 return expect(simpleDao.for(Model).findById("1")).to.eventually.be
672 .rejectedWith("Argument passed in must be a single String of 12 bytes or a string of 24 hex characters");
673 });
674 });
675
676 it("should return an object that is an instance of the provided class, created via the class' .factory() method", async () => {
677 const factorySpy = sandbox.spy(Model, "factory");
678 expect(factorySpy.callCount).to.eql(0);
679
680 const result = await simpleDao.for(Model).findById(modelOne._id);
681 expect(result).to.exist;
682 expect(result).to.be.an.instanceOf(Model);
683 expect(factorySpy.callCount).to.eql(1);
684 });
685
686 it("should return null if there is no document with the specified id", async () => {
687 const result = await simpleDao.for(Model).findById(new ObjectID());
688 expect(result).to.eql(null);
689 });
690 });
691
692 describe(".findAggregate()", () => {
693 describe(".toArray()", () => {
694 it("should return an array of all documents produced by the specified aggregate query", async () => {
695 const query = {$group: {_id: 1, total: {$sum: "$a"}}};
696 const result = await simpleDao.for(Model).findAggregate(query).toArray();
697 expect(result).to.deep.eql([{_id: 1, total: 5}]);
698 });
699
700 it("should return an array of objects that are instances of the provided class, " +
701 "created via the class' .factory() method", async () => {
702 const factorySpy = sandbox.spy(Model, "factory");
703 expect(factorySpy.callCount).to.eql(0);
704
705 const results = await simpleDao.for(Model).findAggregate({$group: {_id: 1, total: {$sum: "$a"}}}).toArray();
706 expect(results).to.have.length.gt(0);
707 expect(factorySpy.callCount).to.eql(results.length);
708
709 for (const data of results) {
710 expect(data).to.be.an.instanceOf(Model);
711 }
712 });
713
714 it("should reject if there was an error performing the query", async () => {
715 return expect(simpleDao.for(Model).findAggregate({a: {$badOperator: 0}}).toArray())
716 .to.eventually.be.rejectedWith("Unrecognized pipeline stage name");
717 });
718 });
719
720 describe(".toCursor()", () => {
721 it("should return a cursor for all documents produced by the specified aggregate query", async () => {
722 const cursor = await simpleDao.for(Model).findAggregate({$match: {a: {$gt: 0}}}).toCursor();
723 expect(cursor.constructor.name).to.eql("AggregationCursor");
724
725 const results = await cursor.toArray();
726 expect(results).to.have.length(3);
727 });
728 });
729 });
730
731 describe(".update()", () => {
732 it("should reject if no query is provided", async () => {
733 return expect(simpleDao.for(Model).update()).to.be.rejectedWith("query can't be undefined or null");
734 });
735
736 it("should reject if no update parameter is provided", async () => {
737 return expect(simpleDao.for(Model).update({})).to.be.rejectedWith("Error: update can't be undefined or null");
738 });
739
740 it("should update only one document by default", async () => {
741 const result = await simpleDao.for(Model).update({}, {$set: {a: 5}});
742 expect(result).to.deep.eql({n: 1, nModified: 1, ok: 1, updatedExisting: true});
743 });
744
745 it("should update multiple documents when the `multi: true` option is provided", async () => {
746 const result = await simpleDao.for(Model).update({}, {$set: {a: 5}}, {multi: true});
747 expect(result).to.deep.eql({n: 3, nModified: 3, ok: 1, updatedExisting: true});
748 });
749
750 it("should not update anything if the provided query matches no documents", async () => {
751 const result = await simpleDao.for(Model).update({b: 1}, {$set: {a: 5}});
752 expect(result).to.deep.eql({n: 0, nModified: 0, ok: 1, updatedExisting: false});
753 });
754
755 it("should reject if the update operation is invalid", async () => {
756 return expect(simpleDao.for(Model).update({b: 1}, {$badOperator: {a: 5}}))
757 .to.be.rejectedWith("Unknown modifier");
758 });
759
760 it("should reject if an error was encountered when connecting to the database", async () => {
761 sandbox.stub(simpleDao, "connect").rejects(new Error("Some connection error"));
762 await expect(simpleDao.for(Model).update({}, {$set: {a: 5}})).to.eventually.be.rejectedWith("Some connection error");
763 });
764 });
765
766 describe(".remove()", () => {
767 it("should remove all documents that match the provided query", async () => {
768 const db = await simpleDao.connect();
769
770 const query = {a: 2};
771 const documentsPriorToRemoval = await db.collection(collectionName).find(query).toArray();
772 expect(documentsPriorToRemoval).to.have.length(2);
773
774 const result = await simpleDao.for(Model).remove(query);
775 expect(result).to.deep.eql({n: 2, ok: 1});
776
777 const documentsAfterRemoval = await db.collection(collectionName).find(query).toArray();
778 expect(documentsAfterRemoval.length).to.eql(0);
779 });
780
781 it("should remove no documents if the provided query matches no documents", async () => {
782 const db = await simpleDao.connect();
783
784 const allDocumentsInCollectionPriorToRemoval = await db.collection(collectionName).find({}).toArray();
785 expect(allDocumentsInCollectionPriorToRemoval).to.have.length(3);
786
787 const query = {a: 5};
788 const result = await simpleDao.for(Model).remove(query);
789 expect(result).to.deep.eql({n: 0, ok: 1});
790
791 const allDocumentsInCollectionAfterRemoval = await db.collection(collectionName).find({}).toArray();
792 expect(allDocumentsInCollectionAfterRemoval.length).to.eql(3);
793 });
794
795 it("should reject if no query is provided", async () => {
796 return expect(simpleDao.for(Model).remove()).to.be.rejectedWith("query can't be undefined or null");
797 });
798
799 it("should reject if the query is invalid", async () => {
800 return expect(simpleDao.for(Model).remove({$badOperator: 1}))
801 .to.be.rejectedWith("unknown top level operator");
802 });
803
804 it("should reject if an error was encountered when connecting to the database", async () => {
805 sandbox.stub(simpleDao, "connect").rejects(new Error("Some connection error"));
806 await expect(simpleDao.for(Model).remove({})).to.eventually.be.rejectedWith("Some connection error");
807 });
808 });
809
810 describe(".removeById()", () => {
811 context("when the provided 'id' is an Object ID", () => {
812 it("should remove the single document that has the specified id", async () => {
813 const result = await simpleDao.for(Model).removeById(modelOne._id);
814 expect(result).to.deep.eql({n: 1, ok: 1});
815 await expectDocumentDoesNotExist(modelOne._id);
816 });
817 });
818
819 context("when the provided 'id' is a string", () => {
820 it("should remove the single document that has the specified id", async () => {
821 const result = await simpleDao.for(Model).removeById(modelOne._id.toString());
822 expect(result).to.deep.eql({n: 1, ok: 1});
823 await expectDocumentDoesNotExist(modelOne._id);
824 });
825
826 it("should reject if the provided string is not a valid Object ID", async () => {
827 return expect(simpleDao.for(Model).removeById("1")).to.eventually.be
828 .rejectedWith("Argument passed in must be a single String of 12 bytes or a string of 24 hex characters");
829 });
830 });
831
832 it("should do nothing if there is no document with the specified id", async () => {
833 const result = await simpleDao.for(Model).removeById(new ObjectID());
834 expect(result).to.deep.eql({n: 0, ok: 1});
835 });
836 });
837
838 describe(".distinct()", () => {
839 it("should return an empty array when no field is provided", async () => {
840 const results = await simpleDao.for(Model).distinct();
841 expect(results).to.deep.eql([]);
842 });
843
844 it("should return all distinct values for the provided field when no query is specified", async () => {
845 const results = await simpleDao.for(Model).distinct("a");
846 expect(results).to.be.an("array").that.includes.members([1, 2]);
847 });
848
849 it("should return the distinct values for the provided field amongst all documents that match the provided query", async () => {
850 const modelFour = Model.factory({a: 3});
851 await simpleDao.save(modelFour);
852 const results = await simpleDao.for(Model).distinct("a", {a: {$gt: 1}});
853 expect(results).to.be.an("array").that.includes.members([2, 3]);
854 });
855
856 it("should reject if the query is invalid", async () => {
857 return expect(simpleDao.for(Model).distinct("a", {$badOperator: 1}))
858 .to.be.rejectedWith("unknown top level operator");
859 });
860
861 it("should reject if an error was encountered when connecting to the database", async () => {
862 sandbox.stub(simpleDao, "connect").rejects(new Error("Some connection error"));
863 await expect(simpleDao.for(Model).distinct("a")).to.eventually.be.rejectedWith("Some connection error");
864 });
865 });
866 });
867});