1 | const Chance = require("chance");
|
2 | const chance = new Chance();
|
3 | const {ObjectID, MongoClient, Cursor} = require("mongodb");
|
4 | const chai = require("chai");
|
5 | const expect = chai.expect;
|
6 | const chaiAsPromised = require("chai-as-promised");
|
7 | chai.use(chaiAsPromised);
|
8 | const sinon = require("sinon");
|
9 | const sandbox = sinon.createSandbox();
|
10 | const {ALL_AUTH_MECHANISMS, ALL_READ_PREFERENCES} = require("../constants");
|
11 | const SimpleDao = require("../").SimpleDao;
|
12 | const {getConnectionString} = require("../src/simple-dao");
|
13 |
|
14 |
|
15 | async function databaseHasCollection(db, collectionName) {
|
16 | const allCollections = await db.listCollections().toArray();
|
17 | return allCollections.some((collection) => { return collection.name === collectionName; });
|
18 | }
|
19 |
|
20 |
|
21 | describe("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 |
|
272 |
|
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 |
|
279 | configForOtherDatabase.db.options.database = "simple_dao_test_3";
|
280 |
|
281 |
|
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 |
|
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 |
|
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 |
|
306 |
|
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 |
|
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 |
|
332 |
|
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 |
|
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 |
|
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 |
|
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 | });
|