meadow-integration 1.0.17 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,580 @@
1
+ /*
2
+ Integration tests for Comprehension Push via Integration Adapter
3
+
4
+ Starts a retold-harness server backed by SQLite (in-memory), generates
5
+ random Book data with fable's DataGeneration service, pushes a
6
+ comprehension through the Integration Adapter, then reads the records
7
+ back from the API to verify they were created.
8
+ */
9
+
10
+ const Chai = require('chai');
11
+ const Expect = Chai.expect;
12
+
13
+ const libFable = require('fable');
14
+ const libMeadowConnectionSQLite = require('meadow-connection-sqlite');
15
+ const libRetoldDataService = require('retold-data-service');
16
+
17
+ const libIntegrationAdapter = require('../source/Meadow-Service-Integration-Adapter.js');
18
+ const libGUIDMap = require('../source/Meadow-Service-Integration-GUIDMap.js');
19
+ const libRestClient = require('../source/services/clone/Meadow-Service-RestClient.js');
20
+
21
+ // Use a unique port to avoid collisions with other test suites
22
+ const _APIServerPort = 19876;
23
+ const _ServerURL = `http://localhost:${_APIServerPort}/1.0/`;
24
+
25
+ let _Fable;
26
+ let _RetoldDataService;
27
+
28
+ suite
29
+ (
30
+ 'Comprehension Push – Random Books',
31
+ function ()
32
+ {
33
+ suiteSetup
34
+ (
35
+ function (fDone)
36
+ {
37
+ this.timeout(15000);
38
+
39
+ let tmpSettings = {
40
+ Product: 'MeadowIntegrationPushTest',
41
+ ProductVersion: '1.0.0',
42
+ APIServerPort: _APIServerPort,
43
+ SQLite:
44
+ {
45
+ SQLiteFilePath: ':memory:'
46
+ },
47
+ LogStreams:
48
+ [
49
+ {
50
+ streamtype: 'console',
51
+ level: 'fatal'
52
+ }
53
+ ]
54
+ };
55
+
56
+ _Fable = new libFable(tmpSettings);
57
+
58
+ // ---- SQLite provider ----
59
+ _Fable.serviceManager.addServiceType('MeadowSQLiteProvider', libMeadowConnectionSQLite);
60
+ _Fable.serviceManager.instantiateServiceProvider('MeadowSQLiteProvider');
61
+
62
+ _Fable.MeadowSQLiteProvider.connectAsync(
63
+ (pError) =>
64
+ {
65
+ if (pError)
66
+ {
67
+ return fDone(pError);
68
+ }
69
+
70
+ let tmpDB = _Fable.MeadowSQLiteProvider.db;
71
+
72
+ // Create only the tables we need for this test
73
+ tmpDB.exec(`
74
+ CREATE TABLE IF NOT EXISTS User (
75
+ IDUser INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ GUIDUser INTEGER DEFAULT 0,
77
+ LoginID TEXT DEFAULT '',
78
+ Password TEXT DEFAULT '',
79
+ NameFirst TEXT DEFAULT '',
80
+ NameLast TEXT DEFAULT '',
81
+ FullName TEXT DEFAULT '',
82
+ Config TEXT DEFAULT ''
83
+ );
84
+ CREATE TABLE IF NOT EXISTS Book (
85
+ IDBook INTEGER PRIMARY KEY AUTOINCREMENT,
86
+ GUIDBook TEXT DEFAULT '',
87
+ CreateDate TEXT DEFAULT '',
88
+ CreatingIDUser INTEGER DEFAULT 0,
89
+ UpdateDate TEXT DEFAULT '',
90
+ UpdatingIDUser INTEGER DEFAULT 0,
91
+ Deleted INTEGER DEFAULT 0,
92
+ DeleteDate TEXT DEFAULT '',
93
+ DeletingIDUser INTEGER DEFAULT 0,
94
+ Title TEXT DEFAULT '',
95
+ Type TEXT DEFAULT '',
96
+ Genre TEXT DEFAULT '',
97
+ ISBN TEXT DEFAULT '',
98
+ Language TEXT DEFAULT '',
99
+ ImageURL TEXT DEFAULT '',
100
+ PublicationYear INTEGER DEFAULT 0
101
+ );
102
+ CREATE TABLE IF NOT EXISTS BookAuthorJoin (
103
+ IDBookAuthorJoin INTEGER PRIMARY KEY AUTOINCREMENT,
104
+ GUIDBookAuthorJoin TEXT DEFAULT '',
105
+ IDBook INTEGER DEFAULT 0,
106
+ IDAuthor INTEGER DEFAULT 0
107
+ );
108
+ CREATE TABLE IF NOT EXISTS Author (
109
+ IDAuthor INTEGER PRIMARY KEY AUTOINCREMENT,
110
+ GUIDAuthor TEXT DEFAULT '',
111
+ CreateDate TEXT DEFAULT '',
112
+ CreatingIDUser INTEGER DEFAULT 0,
113
+ UpdateDate TEXT DEFAULT '',
114
+ UpdatingIDUser INTEGER DEFAULT 0,
115
+ Deleted INTEGER DEFAULT 0,
116
+ DeleteDate TEXT DEFAULT '',
117
+ DeletingIDUser INTEGER DEFAULT 0,
118
+ Name TEXT DEFAULT '',
119
+ IDUser INTEGER DEFAULT 0
120
+ );
121
+ CREATE TABLE IF NOT EXISTS BookPrice (
122
+ IDBookPrice INTEGER PRIMARY KEY AUTOINCREMENT,
123
+ GUIDBookPrice TEXT DEFAULT '',
124
+ CreateDate TEXT DEFAULT '',
125
+ CreatingIDUser INTEGER DEFAULT 0,
126
+ UpdateDate TEXT DEFAULT '',
127
+ UpdatingIDUser INTEGER DEFAULT 0,
128
+ Deleted INTEGER DEFAULT 0,
129
+ DeleteDate TEXT DEFAULT '',
130
+ DeletingIDUser INTEGER DEFAULT 0,
131
+ Price REAL DEFAULT 0,
132
+ StartDate TEXT DEFAULT '',
133
+ EndDate TEXT DEFAULT '',
134
+ Discountable INTEGER DEFAULT 0,
135
+ CouponCode TEXT DEFAULT '',
136
+ IDBook INTEGER DEFAULT 0
137
+ );
138
+ CREATE TABLE IF NOT EXISTS BookStore (
139
+ IDBookStore INTEGER PRIMARY KEY AUTOINCREMENT,
140
+ GUIDBookStore TEXT DEFAULT '',
141
+ CreateDate TEXT DEFAULT '',
142
+ CreatingIDUser INTEGER DEFAULT 0,
143
+ UpdateDate TEXT DEFAULT '',
144
+ UpdatingIDUser INTEGER DEFAULT 0,
145
+ Deleted INTEGER DEFAULT 0,
146
+ DeleteDate TEXT DEFAULT '',
147
+ DeletingIDUser INTEGER DEFAULT 0,
148
+ Name TEXT DEFAULT '',
149
+ Address TEXT DEFAULT '',
150
+ City TEXT DEFAULT '',
151
+ State TEXT DEFAULT '',
152
+ Postal TEXT DEFAULT '',
153
+ Country TEXT DEFAULT ''
154
+ );
155
+ CREATE TABLE IF NOT EXISTS BookStoreInventory (
156
+ IDBookStoreInventory INTEGER PRIMARY KEY AUTOINCREMENT,
157
+ GUIDBookStoreInventory TEXT DEFAULT '',
158
+ CreateDate TEXT DEFAULT '',
159
+ CreatingIDUser INTEGER DEFAULT 0,
160
+ UpdateDate TEXT DEFAULT '',
161
+ UpdatingIDUser INTEGER DEFAULT 0,
162
+ Deleted INTEGER DEFAULT 0,
163
+ DeleteDate TEXT DEFAULT '',
164
+ DeletingIDUser INTEGER DEFAULT 0,
165
+ StockDate TEXT DEFAULT '',
166
+ BookCount INTEGER DEFAULT 0,
167
+ AggregateBookCount INTEGER DEFAULT 0,
168
+ IDBook INTEGER DEFAULT 0,
169
+ IDBookStore INTEGER DEFAULT 0,
170
+ IDBookPrice INTEGER DEFAULT 0,
171
+ StockingAssociate INTEGER DEFAULT 0
172
+ );
173
+ CREATE TABLE IF NOT EXISTS Review (
174
+ IDReview INTEGER PRIMARY KEY AUTOINCREMENT,
175
+ GUIDReview TEXT DEFAULT '',
176
+ CreateDate TEXT DEFAULT '',
177
+ CreatingIDUser INTEGER DEFAULT 0,
178
+ UpdateDate TEXT DEFAULT '',
179
+ UpdatingIDUser INTEGER DEFAULT 0,
180
+ Deleted INTEGER DEFAULT 0,
181
+ DeleteDate TEXT DEFAULT '',
182
+ DeletingIDUser INTEGER DEFAULT 0,
183
+ Text TEXT DEFAULT '',
184
+ Rating INTEGER DEFAULT 0,
185
+ IDBook INTEGER DEFAULT 0,
186
+ IDUser INTEGER DEFAULT 0
187
+ );
188
+ `);
189
+
190
+ // Seed a minimal user so CreatingIDUser 0 is fine
191
+ let tmpInsertUser = tmpDB.prepare(
192
+ `INSERT INTO User (IDUser, GUIDUser, LoginID, Password, NameFirst, NameLast, FullName, Config)
193
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
194
+ tmpInsertUser.run(1, 1001, 'admin', 'hash123', 'Admin', 'User', 'Admin User', '{}');
195
+
196
+ // ---- RetoldDataService ----
197
+ _Fable.serviceManager.addServiceType('RetoldDataService', libRetoldDataService);
198
+ _RetoldDataService = _Fable.serviceManager.instantiateServiceProvider('RetoldDataService',
199
+ {
200
+ FullMeadowSchemaPath: `${__dirname}/../node_modules/retold-harness/source/schemas/bookstore/`,
201
+ FullMeadowSchemaFilename: `Schema.json`,
202
+
203
+ StorageProvider: 'SQLite',
204
+ StorageProviderModule: 'meadow-connection-sqlite',
205
+
206
+ AutoInitializeDataService: true,
207
+ AutoStartOrator: true
208
+ });
209
+
210
+ _RetoldDataService.initializeService(
211
+ (pInitError) =>
212
+ {
213
+ if (pInitError)
214
+ {
215
+ return fDone(pInitError);
216
+ }
217
+ return fDone();
218
+ });
219
+ });
220
+ }
221
+ );
222
+
223
+ suiteTeardown
224
+ (
225
+ function (fDone)
226
+ {
227
+ this.timeout(5000);
228
+ if (_Fable && _Fable.MeadowSQLiteProvider && _Fable.MeadowSQLiteProvider.db)
229
+ {
230
+ try { _Fable.MeadowSQLiteProvider.db.close(); }
231
+ catch (pIgnore) { /* already closed */ }
232
+ }
233
+ if (_Fable && _Fable.OratorServiceServer && _Fable.OratorServiceServer.Active && _Fable.OratorServiceServer.server)
234
+ {
235
+ _Fable.OratorServiceServer.server.close(
236
+ () =>
237
+ {
238
+ _Fable.OratorServiceServer.Active = false;
239
+ fDone();
240
+ });
241
+ }
242
+ else
243
+ {
244
+ fDone();
245
+ }
246
+ }
247
+ );
248
+
249
+ suite
250
+ (
251
+ 'Generate and Push Random Books',
252
+ function ()
253
+ {
254
+ let _GeneratedBooks = [];
255
+ let _BookCount = 10;
256
+
257
+ test
258
+ (
259
+ 'Generate random book records using fable DataGeneration',
260
+ function (fDone)
261
+ {
262
+ _Fable.instantiateServiceProviderIfNotExists('DataGeneration');
263
+ let tmpDataGen = _Fable.DataGeneration;
264
+
265
+ Expect(tmpDataGen).to.be.an('object');
266
+
267
+ let tmpGenres = ['Science Fiction', 'Fantasy', 'Mystery', 'Romance', 'Thriller', 'Horror', 'Historical', 'Comedy'];
268
+ let tmpTypes = ['Fiction', 'Non-Fiction'];
269
+ let tmpLanguages = ['English', 'Spanish', 'French', 'German', 'Japanese'];
270
+
271
+ for (let i = 0; i < _BookCount; i++)
272
+ {
273
+ // Combine random data to create fun book titles like "The Blue Tuesday Chronicles"
274
+ let tmpTitle = `The ${tmpDataGen.randomColor()} ${tmpDataGen.randomDayOfWeek()} of ${tmpDataGen.randomName()} ${tmpDataGen.randomSurname()}`;
275
+ let tmpGenre = tmpGenres[tmpDataGen.randomIntegerUpTo(tmpGenres.length)];
276
+ let tmpType = tmpTypes[tmpDataGen.randomIntegerUpTo(tmpTypes.length)];
277
+ let tmpLanguage = tmpLanguages[tmpDataGen.randomIntegerUpTo(tmpLanguages.length)];
278
+ let tmpYear = tmpDataGen.randomIntegerBetween(1900, 2025);
279
+ let tmpISBN = `978-${tmpDataGen.randomNumericString(10)}`;
280
+
281
+ _GeneratedBooks.push(
282
+ {
283
+ GUIDBook: `RandBook-${i}`,
284
+ Title: tmpTitle,
285
+ Type: tmpType,
286
+ Genre: tmpGenre,
287
+ ISBN: tmpISBN,
288
+ Language: tmpLanguage,
289
+ PublicationYear: tmpYear
290
+ });
291
+ }
292
+
293
+ Expect(_GeneratedBooks.length).to.equal(_BookCount);
294
+
295
+ // Verify each book has the expected structure
296
+ for (let i = 0; i < _GeneratedBooks.length; i++)
297
+ {
298
+ Expect(_GeneratedBooks[i]).to.have.property('GUIDBook');
299
+ Expect(_GeneratedBooks[i]).to.have.property('Title');
300
+ Expect(_GeneratedBooks[i].Title.length).to.be.greaterThan(0);
301
+ }
302
+
303
+ return fDone();
304
+ }
305
+ );
306
+
307
+ test
308
+ (
309
+ 'Push generated books through the Integration Adapter',
310
+ function (fDone)
311
+ {
312
+ this.timeout(30000);
313
+
314
+ // Register services on fable
315
+ _Fable.serviceManager.addServiceType('IntegrationAdapter', libIntegrationAdapter);
316
+ _Fable.serviceManager.addServiceType('MeadowGUIDMap', libGUIDMap);
317
+ _Fable.serviceManager.addServiceType('MeadowCloneRestClient', libRestClient);
318
+
319
+ // Create a REST client pointing at our in-memory harness server
320
+ let tmpRestClient = _Fable.serviceManager.instantiateServiceProvider('MeadowCloneRestClient',
321
+ {
322
+ ServerURL: _ServerURL
323
+ });
324
+
325
+ Expect(tmpRestClient).to.be.an('object');
326
+
327
+ // Create the adapter for the Book entity
328
+ let tmpAdapter = _Fable.serviceManager.instantiateServiceProvider('IntegrationAdapter',
329
+ {
330
+ Entity: 'Book',
331
+ AdapterSetGUIDMarshalPrefix: 'TEST',
332
+ EntityGUIDMarshalPrefix: 'BK',
333
+ ForceMarshal: true
334
+ }, 'Book');
335
+
336
+ tmpAdapter.setRestClient(tmpRestClient);
337
+
338
+ Expect(tmpAdapter).to.be.an('object');
339
+ Expect(tmpAdapter.Entity).to.equal('Book');
340
+
341
+ // Add each generated book as a source record
342
+ for (let i = 0; i < _GeneratedBooks.length; i++)
343
+ {
344
+ tmpAdapter.addSourceRecord(_GeneratedBooks[i]);
345
+ }
346
+
347
+ // Push the records to the server
348
+ tmpAdapter.integrateRecords(
349
+ (pError) =>
350
+ {
351
+ Expect(pError).to.not.be.an('Error');
352
+
353
+ // Verify GUIDs were mapped
354
+ let tmpGUIDMap = _Fable.MeadowGUIDMap;
355
+ for (let i = 0; i < _GeneratedBooks.length; i++)
356
+ {
357
+ let tmpMeadowGUID = tmpGUIDMap.getMeadowGUIDFromExternalGUID('Book', _GeneratedBooks[i].GUIDBook);
358
+ Expect(tmpMeadowGUID).to.be.a('string');
359
+ Expect(tmpMeadowGUID).to.contain('TEST-');
360
+ Expect(tmpMeadowGUID).to.contain('BK-');
361
+ }
362
+
363
+ return fDone();
364
+ });
365
+ }
366
+ );
367
+
368
+ test
369
+ (
370
+ 'Read back the pushed books from the REST API',
371
+ function (fDone)
372
+ {
373
+ this.timeout(15000);
374
+
375
+ let tmpRestClient = _Fable.MeadowCloneRestClient;
376
+ Expect(tmpRestClient).to.be.an('object');
377
+
378
+ // Read each pushed book by its generated Meadow GUID
379
+ let tmpGUIDMap = _Fable.MeadowGUIDMap;
380
+ let tmpRemaining = _GeneratedBooks.length;
381
+ let tmpVerified = 0;
382
+
383
+ for (let i = 0; i < _GeneratedBooks.length; i++)
384
+ {
385
+ let tmpOriginal = _GeneratedBooks[i];
386
+ let tmpMeadowGUID = tmpGUIDMap.getMeadowGUIDFromExternalGUID('Book', tmpOriginal.GUIDBook);
387
+
388
+ tmpRestClient.getEntityByGUID('Book', tmpMeadowGUID,
389
+ (pError, pBody) =>
390
+ {
391
+ Expect(pError).to.not.be.an('Error');
392
+ Expect(pBody).to.be.an('object');
393
+ Expect(pBody.IDBook).to.be.greaterThan(0);
394
+ Expect(pBody.GUIDBook).to.equal(tmpMeadowGUID);
395
+ Expect(pBody.Title).to.equal(tmpOriginal.Title);
396
+ Expect(pBody.Genre).to.equal(tmpOriginal.Genre);
397
+ Expect(pBody.Type).to.equal(tmpOriginal.Type);
398
+ Expect(pBody.Language).to.equal(tmpOriginal.Language);
399
+ Expect(pBody.ISBN).to.equal(tmpOriginal.ISBN);
400
+
401
+ tmpVerified++;
402
+ tmpRemaining--;
403
+
404
+ if (tmpRemaining <= 0)
405
+ {
406
+ Expect(tmpVerified).to.equal(_BookCount);
407
+ return fDone();
408
+ }
409
+ });
410
+ }
411
+ }
412
+ );
413
+
414
+ test
415
+ (
416
+ 'Upsert existing books with updated titles',
417
+ function (fDone)
418
+ {
419
+ this.timeout(30000);
420
+
421
+ // Create a new adapter for the same entity (will re-use the GUIDMap)
422
+ let tmpAdapter = libIntegrationAdapter.getAdapter(_Fable, 'Book', 'BK',
423
+ {
424
+ AdapterSetGUIDMarshalPrefix: 'TEST',
425
+ ForceMarshal: true
426
+ });
427
+
428
+ tmpAdapter.setRestClient(_Fable.MeadowCloneRestClient);
429
+
430
+ // Update the title of each generated book
431
+ for (let i = 0; i < _GeneratedBooks.length; i++)
432
+ {
433
+ let tmpUpdated = Object.assign({}, _GeneratedBooks[i]);
434
+ tmpUpdated.Title = `Updated: ${tmpUpdated.Title}`;
435
+ tmpAdapter.addSourceRecord(tmpUpdated);
436
+ }
437
+
438
+ tmpAdapter.integrateRecords(
439
+ (pError) =>
440
+ {
441
+ Expect(pError).to.not.be.an('Error');
442
+ return fDone();
443
+ });
444
+ }
445
+ );
446
+
447
+ test
448
+ (
449
+ 'Verify updated titles via REST API',
450
+ function (fDone)
451
+ {
452
+ this.timeout(15000);
453
+
454
+ let tmpRestClient = _Fable.MeadowCloneRestClient;
455
+ let tmpGUIDMap = _Fable.MeadowGUIDMap;
456
+ let tmpRemaining = _GeneratedBooks.length;
457
+ let tmpVerified = 0;
458
+
459
+ for (let i = 0; i < _GeneratedBooks.length; i++)
460
+ {
461
+ let tmpOriginal = _GeneratedBooks[i];
462
+ let tmpMeadowGUID = tmpGUIDMap.getMeadowGUIDFromExternalGUID('Book', tmpOriginal.GUIDBook);
463
+
464
+ tmpRestClient.getEntityByGUID('Book', tmpMeadowGUID,
465
+ (pError, pBody) =>
466
+ {
467
+ Expect(pError).to.not.be.an('Error');
468
+ Expect(pBody).to.be.an('object');
469
+ Expect(pBody.GUIDBook).to.equal(tmpMeadowGUID);
470
+ Expect(pBody.Title).to.equal(`Updated: ${tmpOriginal.Title}`);
471
+
472
+ tmpVerified++;
473
+ tmpRemaining--;
474
+
475
+ if (tmpRemaining <= 0)
476
+ {
477
+ Expect(tmpVerified).to.equal(_BookCount);
478
+ return fDone();
479
+ }
480
+ });
481
+ }
482
+ }
483
+ );
484
+
485
+ test
486
+ (
487
+ 'Push authors and join records with FK resolution',
488
+ function (fDone)
489
+ {
490
+ this.timeout(30000);
491
+
492
+ // Generate some random authors
493
+ _Fable.instantiateServiceProviderIfNotExists('DataGeneration');
494
+ let tmpDataGen = _Fable.DataGeneration;
495
+
496
+ // Create 3 random authors
497
+ let tmpAuthors = [];
498
+ for (let i = 0; i < 3; i++)
499
+ {
500
+ tmpAuthors.push(
501
+ {
502
+ GUIDAuthor: `RandAuthor-${i}`,
503
+ Name: `${tmpDataGen.randomName()} ${tmpDataGen.randomSurname()}`
504
+ });
505
+ }
506
+
507
+ // Push authors first
508
+ let tmpAuthorAdapter = libIntegrationAdapter.getAdapter(_Fable, 'Author', 'AU',
509
+ {
510
+ AdapterSetGUIDMarshalPrefix: 'TEST',
511
+ ForceMarshal: true
512
+ });
513
+ tmpAuthorAdapter.setRestClient(_Fable.MeadowCloneRestClient);
514
+
515
+ for (let i = 0; i < tmpAuthors.length; i++)
516
+ {
517
+ tmpAuthorAdapter.addSourceRecord(tmpAuthors[i]);
518
+ }
519
+
520
+ tmpAuthorAdapter.integrateRecords(
521
+ (pAuthorError) =>
522
+ {
523
+ Expect(pAuthorError).to.not.be.an('Error');
524
+
525
+ // Now create BookAuthorJoin records that reference both
526
+ // Book (via external GUID) and Author (via external GUID)
527
+ let tmpJoinAdapter = libIntegrationAdapter.getAdapter(_Fable, 'BookAuthorJoin', 'BAJ',
528
+ {
529
+ AdapterSetGUIDMarshalPrefix: 'TEST',
530
+ ForceMarshal: true
531
+ });
532
+ tmpJoinAdapter.setRestClient(_Fable.MeadowCloneRestClient);
533
+
534
+ // Link first 3 books to the 3 authors
535
+ for (let i = 0; i < 3; i++)
536
+ {
537
+ tmpJoinAdapter.addSourceRecord(
538
+ {
539
+ GUIDBookAuthorJoin: `RandJoin-${i}`,
540
+ GUIDBook: `RandBook-${i}`,
541
+ GUIDAuthor: `RandAuthor-${i}`
542
+ });
543
+ }
544
+
545
+ tmpJoinAdapter.integrateRecords(
546
+ (pJoinError) =>
547
+ {
548
+ Expect(pJoinError).to.not.be.an('Error');
549
+
550
+ // Verify the join records were created with correct FK IDs
551
+ let tmpGUIDMap = _Fable.MeadowGUIDMap;
552
+ let tmpRestClient = _Fable.MeadowCloneRestClient;
553
+ let tmpRemaining = 3;
554
+
555
+ for (let i = 0; i < 3; i++)
556
+ {
557
+ let tmpJoinGUID = tmpGUIDMap.getMeadowGUIDFromExternalGUID('BookAuthorJoin', `RandJoin-${i}`);
558
+ tmpRestClient.getEntityByGUID('BookAuthorJoin', tmpJoinGUID,
559
+ (pReadError, pJoinBody) =>
560
+ {
561
+ Expect(pReadError).to.not.be.an('Error');
562
+ Expect(pJoinBody).to.be.an('object');
563
+ Expect(pJoinBody.IDBook).to.be.greaterThan(0);
564
+ Expect(pJoinBody.IDAuthor).to.be.greaterThan(0);
565
+
566
+ tmpRemaining--;
567
+ if (tmpRemaining <= 0)
568
+ {
569
+ return fDone();
570
+ }
571
+ });
572
+ }
573
+ });
574
+ });
575
+ }
576
+ );
577
+ }
578
+ );
579
+ }
580
+ );