meadow-endpoints 4.0.7 → 4.0.10

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.
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Unit tests for Meadow Endpoints
3
3
  *
4
+ * Uses SQLite (better-sqlite3) as the provider so no external database server
5
+ * is required.
6
+ *
4
7
  * @license MIT
5
8
  *
6
9
  * @author Steven Velozo <steven@velozo.com>
@@ -10,22 +13,170 @@ var Chai = require("chai");
10
13
  var Expect = Chai.expect;
11
14
  var Assert = Chai.assert;
12
15
 
13
- const libAsync = require('async');
14
-
15
- const libBookServer = require('../test_support/bookstore-serve-meadow-endpoint-apis.js');
16
- let _BookServer = false;
16
+ const libFable = require('fable');
17
+ const libOrator = require('orator');
18
+ const libOratorServiceServerRestify = require('orator-serviceserver-restify');
19
+ const libMeadow = require('meadow');
20
+ const libMeadowEndpoints = require('../source/Meadow-Endpoints.js');
21
+ const libMeadowConnectionSQLite = require('meadow-connection-sqlite');
22
+ const libSuperTest = require('supertest');
17
23
 
18
- let _INITIALIZATION_COMPLETE = false;
24
+ const _BookSchema = require('../test_support/model/meadow_schema/BookStore-MeadowSchema-Book.json');
19
25
 
20
- const libMeadowEndpoints = require('../source/Meadow-Endpoints.js');
26
+ // -- Shared state for the test suite --
27
+ let _Fable = false;
28
+ let _Orator = false;
29
+ let _Meadow = false;
30
+ let _MeadowEndpoints = false;
31
+ let _SuperTest = false;
21
32
 
22
- const libSuperTest = require('supertest');
33
+ // Keep track of the API server port
34
+ const _APIServerPort = 9876;
35
+ const _BaseURL = `http://localhost:${_APIServerPort}/`;
23
36
 
24
37
  suite
25
38
  (
26
- 'Meadow-Endpoints-Core',
39
+ 'Meadow-Endpoints',
27
40
  () =>
28
41
  {
42
+ // Set up Fable + Orator + SQLite + Meadow + Endpoints before all tests
43
+ suiteSetup
44
+ (
45
+ function (fDone)
46
+ {
47
+ this.timeout(30000);
48
+
49
+ let tmpSettings = {
50
+ Product: 'MeadowEndpointsTest',
51
+ ProductVersion: '1.0.0',
52
+ APIServerPort: _APIServerPort,
53
+ SQLite:
54
+ {
55
+ SQLiteFilePath: ':memory:'
56
+ },
57
+ LogStreams:
58
+ [
59
+ {
60
+ streamtype: 'console',
61
+ level: 'fatal'
62
+ }
63
+ ],
64
+ MeadowEndpointsSessionDataSource: 'None'
65
+ };
66
+
67
+ _Fable = new libFable(tmpSettings);
68
+
69
+ // Register the Restify service server so Orator uses it
70
+ _Fable.serviceManager.addServiceType('OratorServiceServer', libOratorServiceServerRestify);
71
+
72
+ // Register and instantiate the SQLite connection provider
73
+ _Fable.serviceManager.addServiceType('MeadowSQLiteProvider', libMeadowConnectionSQLite);
74
+ _Fable.serviceManager.instantiateServiceProvider('MeadowSQLiteProvider');
75
+
76
+ _Fable.MeadowSQLiteProvider.connectAsync(
77
+ (pError) =>
78
+ {
79
+ if (pError)
80
+ {
81
+ return fDone(pError);
82
+ }
83
+
84
+ let tmpDB = _Fable.MeadowSQLiteProvider.db;
85
+
86
+ // Create the Book table
87
+ tmpDB.exec(
88
+ `CREATE TABLE IF NOT EXISTS Book (
89
+ IDBook INTEGER PRIMARY KEY AUTOINCREMENT,
90
+ GUIDBook TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000',
91
+ CreateDate TEXT,
92
+ CreatingIDUser INTEGER NOT NULL DEFAULT 0,
93
+ UpdateDate TEXT,
94
+ UpdatingIDUser INTEGER NOT NULL DEFAULT 0,
95
+ Deleted INTEGER NOT NULL DEFAULT 0,
96
+ DeleteDate TEXT,
97
+ DeletingIDUser INTEGER NOT NULL DEFAULT 0,
98
+ Title TEXT NOT NULL DEFAULT '',
99
+ Type TEXT NOT NULL DEFAULT '',
100
+ Genre TEXT NOT NULL DEFAULT '',
101
+ ISBN TEXT NOT NULL DEFAULT '',
102
+ Language TEXT NOT NULL DEFAULT '',
103
+ ImageURL TEXT NOT NULL DEFAULT '',
104
+ PublicationYear INTEGER NOT NULL DEFAULT 0
105
+ );`
106
+ );
107
+
108
+ // Seed some test data
109
+ let tmpInsert = tmpDB.prepare(
110
+ `INSERT INTO Book (Title, Type, Genre, ISBN, Language, PublicationYear)
111
+ VALUES (?, ?, ?, ?, ?, ?)`
112
+ );
113
+ tmpInsert.run('Angels & Demons', 'Novel', 'Thriller', '978-0671027360', 'English', 2000);
114
+ tmpInsert.run('Dune', 'Novel', 'Science Fiction', '978-0441013593', 'English', 1965);
115
+ tmpInsert.run('Neuromancer', 'Novel', 'Science Fiction', '978-0441569595', 'English', 1984);
116
+ tmpInsert.run('Snow Crash', 'Novel', 'Science Fiction', '978-0553380958', 'English', 1992);
117
+ tmpInsert.run('The Da Vinci Code', 'Novel', 'Thriller', '978-0307474278', 'English', 2003);
118
+
119
+ // Create the Meadow DAL and endpoints
120
+ _Meadow = libMeadow.new(_Fable, 'Book')
121
+ .setProvider('SQLite')
122
+ .setSchema(_BookSchema.Schema)
123
+ .setJsonSchema(_BookSchema.JsonSchema)
124
+ .setDefaultIdentifier(_BookSchema.DefaultIdentifier)
125
+ .setDefault(_BookSchema.DefaultObject);
126
+
127
+ // Initialize Orator (creates and registers the Restify service server)
128
+ _Orator = new libOrator(_Fable, {});
129
+ _Orator.initialize(
130
+ () =>
131
+ {
132
+ // Wire up the Meadow Endpoints
133
+ _MeadowEndpoints = libMeadowEndpoints.new(_Meadow);
134
+ _MeadowEndpoints.connectRoutes(_Orator.serviceServer);
135
+
136
+ // Start listening for requests
137
+ _Orator.startService(
138
+ (pStartError) =>
139
+ {
140
+ if (pStartError)
141
+ {
142
+ return fDone(pStartError);
143
+ }
144
+ _SuperTest = libSuperTest(_BaseURL);
145
+ return fDone();
146
+ });
147
+ });
148
+ });
149
+ }
150
+ );
151
+
152
+ suiteTeardown
153
+ (
154
+ function (fDone)
155
+ {
156
+ this.timeout(10000);
157
+ if (_Fable && _Fable.MeadowSQLiteProvider && _Fable.MeadowSQLiteProvider.db)
158
+ {
159
+ try { _Fable.MeadowSQLiteProvider.db.close(); } catch (pIgnore) { /* ignore */ }
160
+ }
161
+ if (_Orator && _Orator.serviceServer && _Orator.serviceServer.Active && _Orator.serviceServer.server)
162
+ {
163
+ // Directly close the restify server to avoid hanging on keep-alive connections
164
+ _Orator.serviceServer.server.close(() =>
165
+ {
166
+ _Orator.serviceServer.Active = false;
167
+ fDone();
168
+ });
169
+ }
170
+ else
171
+ {
172
+ fDone();
173
+ }
174
+ }
175
+ );
176
+
177
+ // ======================================================================
178
+ // Object Sanity
179
+ // ======================================================================
29
180
  suite
30
181
  (
31
182
  'Object Sanity',
@@ -36,48 +187,1446 @@ suite
36
187
  'The class should initialize itself into a happy little object.',
37
188
  function (fDone)
38
189
  {
39
- Expect(true).to.equal(true);
190
+ Expect(_MeadowEndpoints).to.be.an('object');
191
+ Expect(_MeadowEndpoints.DAL).to.be.an('object');
192
+ Expect(_MeadowEndpoints.DAL.scope).to.equal('Book');
193
+ Expect(_MeadowEndpoints.controller).to.be.an('object');
40
194
  fDone();
41
195
  }
42
196
  );
43
197
  test
44
198
  (
45
- 'read: get a specific record',
46
- function(fDone)
199
+ 'The constructor should throw without a valid Meadow DAL.',
200
+ function (fDone)
47
201
  {
48
- libSuperTest('http://localhost:8086/')
49
- .get('1.0/Book/1')
50
- .end(
51
- function (pError, pResponse)
52
- {
53
- var tmpResult = JSON.parse(pResponse.text);
54
- Expect(tmpResult.Title).to.equal('Angels & Demons');
55
- fDone();
56
- }
57
- );
202
+ Expect(() => { new libMeadowEndpoints(); }).to.throw();
203
+ fDone();
58
204
  }
59
205
  );
60
206
  test
61
207
  (
62
- 'create: create a record',
63
- function(fDone)
208
+ 'The MeadowEndpoints class should expose Meadow and BaseController.',
209
+ function (fDone)
64
210
  {
65
- var tmpRecord = {Title:'Batman is Batman'};
66
- libSuperTest('http://localhost:8086/')
67
- .post('1.0/Book')
68
- .send(tmpRecord)
69
- .end(
70
- function(pError, pResponse)
71
- {
72
- // Expect response to be the record we just created.
73
- var tmpResult = JSON.parse(pResponse.text);
74
- Expect(tmpResult.Title).to.equal('Batman is Batman');
75
- fDone();
76
- }
77
- );
211
+ Expect(libMeadowEndpoints.Meadow).to.be.an('object');
212
+ Expect(libMeadowEndpoints.Meadow.new).to.be.a('function');
213
+ Expect(libMeadowEndpoints.BaseController).to.be.a('function');
214
+ Expect(libMeadowEndpoints.new).to.be.a('function');
215
+ fDone();
216
+ }
217
+ );
218
+ test
219
+ (
220
+ 'The endpoint version and prefix should be configured.',
221
+ function (fDone)
222
+ {
223
+ Expect(_MeadowEndpoints.EndpointVersion).to.equal('1.0');
224
+ Expect(_MeadowEndpoints.EndpointName).to.equal('Book');
225
+ Expect(_MeadowEndpoints.EndpointPrefix).to.equal('/1.0/Book');
226
+ fDone();
227
+ }
228
+ );
229
+ test
230
+ (
231
+ 'Behavior sets should all be enabled by default.',
232
+ function (fDone)
233
+ {
234
+ Expect(_MeadowEndpoints._EnabledBehaviorSets.Create).to.equal(true);
235
+ Expect(_MeadowEndpoints._EnabledBehaviorSets.Read).to.equal(true);
236
+ Expect(_MeadowEndpoints._EnabledBehaviorSets.Reads).to.equal(true);
237
+ Expect(_MeadowEndpoints._EnabledBehaviorSets.Update).to.equal(true);
238
+ Expect(_MeadowEndpoints._EnabledBehaviorSets.Delete).to.equal(true);
239
+ Expect(_MeadowEndpoints._EnabledBehaviorSets.Count).to.equal(true);
240
+ Expect(_MeadowEndpoints._EnabledBehaviorSets.Schema).to.equal(true);
241
+ fDone();
242
+ }
243
+ );
244
+ }
245
+ );
246
+
247
+ // ======================================================================
248
+ // Create Endpoints
249
+ // ======================================================================
250
+ suite
251
+ (
252
+ 'Create',
253
+ () =>
254
+ {
255
+ test
256
+ (
257
+ 'create: create a single record',
258
+ function (fDone)
259
+ {
260
+ _SuperTest
261
+ .post('1.0/Book')
262
+ .send({ Title: 'Batman is Batman', Genre: 'Comic', PublicationYear: 1939 })
263
+ .end(
264
+ (pError, pResponse) =>
265
+ {
266
+ Expect(pError).to.not.exist;
267
+ let tmpResult = JSON.parse(pResponse.text);
268
+ Expect(tmpResult.Title).to.equal('Batman is Batman');
269
+ Expect(tmpResult.Genre).to.equal('Comic');
270
+ Expect(tmpResult.PublicationYear).to.equal(1939);
271
+ Expect(tmpResult.IDBook).to.be.above(0);
272
+ Expect(tmpResult.GUIDBook).to.be.a('string');
273
+ Expect(tmpResult.Deleted).to.equal(0);
274
+ fDone();
275
+ }
276
+ );
277
+ }
278
+ );
279
+ test
280
+ (
281
+ 'create: create a record with minimal fields',
282
+ function (fDone)
283
+ {
284
+ _SuperTest
285
+ .post('1.0/Book')
286
+ .send({ Title: 'Minimal Book' })
287
+ .end(
288
+ (pError, pResponse) =>
289
+ {
290
+ let tmpResult = JSON.parse(pResponse.text);
291
+ Expect(tmpResult.Title).to.equal('Minimal Book');
292
+ Expect(tmpResult.IDBook).to.be.above(0);
293
+ fDone();
294
+ }
295
+ );
296
+ }
297
+ );
298
+ test
299
+ (
300
+ 'create: should handle an empty body gracefully',
301
+ function (fDone)
302
+ {
303
+ _SuperTest
304
+ .post('1.0/Book')
305
+ .send({})
306
+ .end(
307
+ (pError, pResponse) =>
308
+ {
309
+ let tmpResult = JSON.parse(pResponse.text);
310
+ // An empty body should still create a record with defaults
311
+ Expect(tmpResult.IDBook).to.be.above(0);
312
+ Expect(tmpResult.Title).to.equal('');
313
+ fDone();
314
+ }
315
+ );
316
+ }
317
+ );
318
+ test
319
+ (
320
+ 'create bulk: create multiple records',
321
+ function (fDone)
322
+ {
323
+ _SuperTest
324
+ .post('1.0/Books')
325
+ .send([
326
+ { Title: 'Bulk Book A', Genre: 'Fantasy' },
327
+ { Title: 'Bulk Book B', Genre: 'Romance' },
328
+ { Title: 'Bulk Book C', Genre: 'Horror' }
329
+ ])
330
+ .end(
331
+ (pError, pResponse) =>
332
+ {
333
+ let tmpResult = JSON.parse(pResponse.text);
334
+ Expect(tmpResult).to.be.an('array');
335
+ Expect(tmpResult.length).to.equal(3);
336
+ Expect(tmpResult[0].Title).to.equal('Bulk Book A');
337
+ Expect(tmpResult[1].Title).to.equal('Bulk Book B');
338
+ Expect(tmpResult[2].Title).to.equal('Bulk Book C');
339
+ fDone();
340
+ }
341
+ );
342
+ }
343
+ );
344
+ }
345
+ );
346
+
347
+ // ======================================================================
348
+ // Read Endpoints
349
+ // ======================================================================
350
+ suite
351
+ (
352
+ 'Read',
353
+ () =>
354
+ {
355
+ test
356
+ (
357
+ 'read: get a specific record by ID',
358
+ function (fDone)
359
+ {
360
+ _SuperTest
361
+ .get('1.0/Book/1')
362
+ .end(
363
+ (pError, pResponse) =>
364
+ {
365
+ let tmpResult = JSON.parse(pResponse.text);
366
+ Expect(tmpResult.IDBook).to.equal(1);
367
+ Expect(tmpResult.Title).to.equal('Angels & Demons');
368
+ fDone();
369
+ }
370
+ );
371
+ }
372
+ );
373
+ test
374
+ (
375
+ 'read: get a second record by ID',
376
+ function (fDone)
377
+ {
378
+ _SuperTest
379
+ .get('1.0/Book/2')
380
+ .end(
381
+ (pError, pResponse) =>
382
+ {
383
+ let tmpResult = JSON.parse(pResponse.text);
384
+ Expect(tmpResult.IDBook).to.equal(2);
385
+ Expect(tmpResult.Title).to.equal('Dune');
386
+ fDone();
387
+ }
388
+ );
389
+ }
390
+ );
391
+ test
392
+ (
393
+ 'read: get a record that does not exist',
394
+ function (fDone)
395
+ {
396
+ _SuperTest
397
+ .get('1.0/Book/99999')
398
+ .end(
399
+ (pError, pResponse) =>
400
+ {
401
+ let tmpResult = JSON.parse(pResponse.text);
402
+ // Should return an error
403
+ Expect(tmpResult).to.have.property('Error');
404
+ fDone();
405
+ }
406
+ );
407
+ }
408
+ );
409
+ test
410
+ (
411
+ 'reads: get all records with default pagination',
412
+ function (fDone)
413
+ {
414
+ _SuperTest
415
+ .get('1.0/Books')
416
+ .end(
417
+ (pError, pResponse) =>
418
+ {
419
+ let tmpResult = JSON.parse(pResponse.text);
420
+ Expect(tmpResult).to.be.an('array');
421
+ // We seeded 5 + created ~4 in prior tests
422
+ Expect(tmpResult.length).to.be.at.least(5);
423
+ fDone();
424
+ }
425
+ );
426
+ }
427
+ );
428
+ test
429
+ (
430
+ 'reads: get records with pagination',
431
+ function (fDone)
432
+ {
433
+ _SuperTest
434
+ .get('1.0/Books/0/2')
435
+ .end(
436
+ (pError, pResponse) =>
437
+ {
438
+ let tmpResult = JSON.parse(pResponse.text);
439
+ Expect(tmpResult).to.be.an('array');
440
+ Expect(tmpResult.length).to.equal(2);
441
+ Expect(tmpResult[0].IDBook).to.equal(1);
442
+ fDone();
443
+ }
444
+ );
445
+ }
446
+ );
447
+ test
448
+ (
449
+ 'reads: get records with offset pagination',
450
+ function (fDone)
451
+ {
452
+ _SuperTest
453
+ .get('1.0/Books/2/2')
454
+ .end(
455
+ (pError, pResponse) =>
456
+ {
457
+ let tmpResult = JSON.parse(pResponse.text);
458
+ Expect(tmpResult).to.be.an('array');
459
+ Expect(tmpResult.length).to.equal(2);
460
+ Expect(tmpResult[0].IDBook).to.equal(3);
461
+ fDone();
462
+ }
463
+ );
464
+ }
465
+ );
466
+ test
467
+ (
468
+ 'reads: get records with filter (contains)',
469
+ function (fDone)
470
+ {
471
+ _SuperTest
472
+ .get('1.0/Books/FilteredTo/FBV~Genre~LK~%25Science%25')
473
+ .end(
474
+ (pError, pResponse) =>
475
+ {
476
+ let tmpResult = JSON.parse(pResponse.text);
477
+ Expect(tmpResult).to.be.an('array');
478
+ Expect(tmpResult.length).to.be.at.least(3);
479
+ tmpResult.forEach((pRecord) =>
480
+ {
481
+ Expect(pRecord.Genre).to.contain('Science');
482
+ });
483
+ fDone();
484
+ }
485
+ );
486
+ }
487
+ );
488
+ test
489
+ (
490
+ 'reads: get records with filter and pagination',
491
+ function (fDone)
492
+ {
493
+ _SuperTest
494
+ .get('1.0/Books/FilteredTo/FBV~Genre~LK~%25Science%25/0/1')
495
+ .end(
496
+ (pError, pResponse) =>
497
+ {
498
+ let tmpResult = JSON.parse(pResponse.text);
499
+ Expect(tmpResult).to.be.an('array');
500
+ Expect(tmpResult.length).to.equal(1);
501
+ Expect(tmpResult[0].Genre).to.contain('Science');
502
+ fDone();
503
+ }
504
+ );
505
+ }
506
+ );
507
+ test
508
+ (
509
+ 'reads by: get records filtered by a field value',
510
+ function (fDone)
511
+ {
512
+ _SuperTest
513
+ .get('1.0/Books/By/Genre/Thriller')
514
+ .end(
515
+ (pError, pResponse) =>
516
+ {
517
+ let tmpResult = JSON.parse(pResponse.text);
518
+ Expect(tmpResult).to.be.an('array');
519
+ Expect(tmpResult.length).to.be.at.least(2);
520
+ tmpResult.forEach((pRecord) =>
521
+ {
522
+ Expect(pRecord.Genre).to.equal('Thriller');
523
+ });
524
+ fDone();
525
+ }
526
+ );
527
+ }
528
+ );
529
+ test
530
+ (
531
+ 'reads by: with pagination',
532
+ function (fDone)
533
+ {
534
+ _SuperTest
535
+ .get('1.0/Books/By/Genre/Thriller/0/1')
536
+ .end(
537
+ (pError, pResponse) =>
538
+ {
539
+ let tmpResult = JSON.parse(pResponse.text);
540
+ Expect(tmpResult).to.be.an('array');
541
+ Expect(tmpResult.length).to.equal(1);
542
+ Expect(tmpResult[0].Genre).to.equal('Thriller');
543
+ fDone();
544
+ }
545
+ );
546
+ }
547
+ );
548
+ }
549
+ );
550
+
551
+ // ======================================================================
552
+ // Specialized Read Endpoints
553
+ // ======================================================================
554
+ suite
555
+ (
556
+ 'Specialized Read',
557
+ () =>
558
+ {
559
+ test
560
+ (
561
+ 'select list: get Hash/Value pairs',
562
+ function (fDone)
563
+ {
564
+ _SuperTest
565
+ .get('1.0/BookSelect')
566
+ .end(
567
+ (pError, pResponse) =>
568
+ {
569
+ let tmpResult = JSON.parse(pResponse.text);
570
+ Expect(tmpResult).to.be.an('array');
571
+ Expect(tmpResult.length).to.be.at.least(5);
572
+ Expect(tmpResult[0]).to.have.property('Hash');
573
+ Expect(tmpResult[0]).to.have.property('Value');
574
+ fDone();
575
+ }
576
+ );
577
+ }
578
+ );
579
+ test
580
+ (
581
+ 'select list: with pagination',
582
+ function (fDone)
583
+ {
584
+ _SuperTest
585
+ .get('1.0/BookSelect/0/2')
586
+ .end(
587
+ (pError, pResponse) =>
588
+ {
589
+ let tmpResult = JSON.parse(pResponse.text);
590
+ Expect(tmpResult).to.be.an('array');
591
+ Expect(tmpResult.length).to.equal(2);
592
+ fDone();
593
+ }
594
+ );
595
+ }
596
+ );
597
+ test
598
+ (
599
+ 'select list: with filter',
600
+ function (fDone)
601
+ {
602
+ _SuperTest
603
+ .get('1.0/BookSelect/FilteredTo/FBV~Genre~EQ~Thriller')
604
+ .end(
605
+ (pError, pResponse) =>
606
+ {
607
+ let tmpResult = JSON.parse(pResponse.text);
608
+ Expect(tmpResult).to.be.an('array');
609
+ Expect(tmpResult.length).to.be.at.least(2);
610
+ tmpResult.forEach((pRecord) =>
611
+ {
612
+ Expect(pRecord).to.have.property('Hash');
613
+ Expect(pRecord).to.have.property('Value');
614
+ });
615
+ fDone();
616
+ }
617
+ );
618
+ }
619
+ );
620
+ test
621
+ (
622
+ 'lite list: get minimal columns',
623
+ function (fDone)
624
+ {
625
+ _SuperTest
626
+ .get('1.0/Books/Lite')
627
+ .end(
628
+ (pError, pResponse) =>
629
+ {
630
+ let tmpResult = JSON.parse(pResponse.text);
631
+ Expect(tmpResult).to.be.an('array');
632
+ Expect(tmpResult.length).to.be.at.least(5);
633
+ fDone();
634
+ }
635
+ );
636
+ }
637
+ );
638
+ test
639
+ (
640
+ 'lite list: with pagination',
641
+ function (fDone)
642
+ {
643
+ _SuperTest
644
+ .get('1.0/Books/Lite/0/3')
645
+ .end(
646
+ (pError, pResponse) =>
647
+ {
648
+ let tmpResult = JSON.parse(pResponse.text);
649
+ Expect(tmpResult).to.be.an('array');
650
+ Expect(tmpResult.length).to.equal(3);
651
+ fDone();
652
+ }
653
+ );
654
+ }
655
+ );
656
+ test
657
+ (
658
+ 'max: get the maximum value for a column',
659
+ function (fDone)
660
+ {
661
+ _SuperTest
662
+ .get('1.0/Book/Max/PublicationYear')
663
+ .end(
664
+ (pError, pResponse) =>
665
+ {
666
+ let tmpResult = JSON.parse(pResponse.text);
667
+ // ReadMax returns a single record (not an array)
668
+ Expect(tmpResult).to.be.an('object');
669
+ Expect(tmpResult).to.have.property('PublicationYear');
670
+ // The max publication year in our seed data is 2003
671
+ Expect(tmpResult.PublicationYear).to.be.at.least(2003);
672
+ fDone();
673
+ }
674
+ );
675
+ }
676
+ );
677
+ }
678
+ );
679
+
680
+ // ======================================================================
681
+ // Update Endpoints
682
+ // ======================================================================
683
+ suite
684
+ (
685
+ 'Update',
686
+ () =>
687
+ {
688
+ test
689
+ (
690
+ 'update: update a single record',
691
+ function (fDone)
692
+ {
693
+ _SuperTest
694
+ .put('1.0/Book')
695
+ .send({ IDBook: 1, Title: 'Angels & Demons (Updated)' })
696
+ .end(
697
+ (pError, pResponse) =>
698
+ {
699
+ let tmpResult = JSON.parse(pResponse.text);
700
+ Expect(tmpResult.IDBook).to.equal(1);
701
+ Expect(tmpResult.Title).to.equal('Angels & Demons (Updated)');
702
+ fDone();
703
+ }
704
+ );
705
+ }
706
+ );
707
+ test
708
+ (
709
+ 'update: verify the update persisted',
710
+ function (fDone)
711
+ {
712
+ _SuperTest
713
+ .get('1.0/Book/1')
714
+ .end(
715
+ (pError, pResponse) =>
716
+ {
717
+ let tmpResult = JSON.parse(pResponse.text);
718
+ Expect(tmpResult.Title).to.equal('Angels & Demons (Updated)');
719
+ fDone();
720
+ }
721
+ );
722
+ }
723
+ );
724
+ test
725
+ (
726
+ 'update: fail without a valid record ID',
727
+ function (fDone)
728
+ {
729
+ _SuperTest
730
+ .put('1.0/Book')
731
+ .send({ Title: 'No ID Provided' })
732
+ .end(
733
+ (pError, pResponse) =>
734
+ {
735
+ let tmpResult = JSON.parse(pResponse.text);
736
+ Expect(tmpResult).to.have.property('Error');
737
+ fDone();
738
+ }
739
+ );
740
+ }
741
+ );
742
+ test
743
+ (
744
+ 'update: fail for a non-existent record',
745
+ function (fDone)
746
+ {
747
+ _SuperTest
748
+ .put('1.0/Book')
749
+ .send({ IDBook: 99999, Title: 'Ghost Record' })
750
+ .end(
751
+ (pError, pResponse) =>
752
+ {
753
+ let tmpResult = JSON.parse(pResponse.text);
754
+ Expect(tmpResult).to.have.property('Error');
755
+ fDone();
756
+ }
757
+ );
758
+ }
759
+ );
760
+ test
761
+ (
762
+ 'bulk update: update multiple records',
763
+ function (fDone)
764
+ {
765
+ _SuperTest
766
+ .put('1.0/Books')
767
+ .send([
768
+ { IDBook: 2, Title: 'Dune (Updated)' },
769
+ { IDBook: 3, Title: 'Neuromancer (Updated)' }
770
+ ])
771
+ .end(
772
+ (pError, pResponse) =>
773
+ {
774
+ let tmpResult = JSON.parse(pResponse.text);
775
+ Expect(tmpResult).to.be.an('array');
776
+ Expect(tmpResult.length).to.equal(2);
777
+ Expect(tmpResult[0].Title).to.equal('Dune (Updated)');
778
+ Expect(tmpResult[1].Title).to.equal('Neuromancer (Updated)');
779
+ fDone();
780
+ }
781
+ );
782
+ }
783
+ );
784
+ test
785
+ (
786
+ 'upsert: update an existing record via upsert',
787
+ function (fDone)
788
+ {
789
+ _SuperTest
790
+ .put('1.0/Book/Upsert')
791
+ .send({ IDBook: 4, Title: 'Snow Crash (Upserted)' })
792
+ .end(
793
+ (pError, pResponse) =>
794
+ {
795
+ let tmpResult = JSON.parse(pResponse.text);
796
+ Expect(tmpResult.IDBook).to.equal(4);
797
+ Expect(tmpResult.Title).to.equal('Snow Crash (Upserted)');
798
+ fDone();
799
+ }
800
+ );
801
+ }
802
+ );
803
+ test
804
+ (
805
+ 'upsert: create a new record via upsert',
806
+ function (fDone)
807
+ {
808
+ _SuperTest
809
+ .put('1.0/Book/Upsert')
810
+ .send({ IDBook: 0, Title: 'Upserted New Book', Genre: 'Upsert' })
811
+ .end(
812
+ (pError, pResponse) =>
813
+ {
814
+ let tmpResult = JSON.parse(pResponse.text);
815
+ Expect(tmpResult.Title).to.equal('Upserted New Book');
816
+ Expect(tmpResult.IDBook).to.be.above(0);
817
+ fDone();
818
+ }
819
+ );
820
+ }
821
+ );
822
+ }
823
+ );
824
+
825
+ // ======================================================================
826
+ // Delete Endpoints
827
+ // ======================================================================
828
+ suite
829
+ (
830
+ 'Delete',
831
+ () =>
832
+ {
833
+ let _DeletedRecordID = 0;
834
+
835
+ test
836
+ (
837
+ 'delete: create a record to delete',
838
+ function (fDone)
839
+ {
840
+ _SuperTest
841
+ .post('1.0/Book')
842
+ .send({ Title: 'To Be Deleted', Genre: 'Temp' })
843
+ .end(
844
+ (pError, pResponse) =>
845
+ {
846
+ let tmpResult = JSON.parse(pResponse.text);
847
+ _DeletedRecordID = tmpResult.IDBook;
848
+ Expect(_DeletedRecordID).to.be.above(0);
849
+ fDone();
850
+ }
851
+ );
852
+ }
853
+ );
854
+ test
855
+ (
856
+ 'delete: delete a record by URL parameter',
857
+ function (fDone)
858
+ {
859
+ _SuperTest
860
+ .delete(`1.0/Book/${_DeletedRecordID}`)
861
+ .end(
862
+ (pError, pResponse) =>
863
+ {
864
+ let tmpResult = JSON.parse(pResponse.text);
865
+ Expect(tmpResult).to.have.property('Count');
866
+ Expect(tmpResult.Count).to.equal(1);
867
+ fDone();
868
+ }
869
+ );
870
+ }
871
+ );
872
+ test
873
+ (
874
+ 'delete: the deleted record should not be readable',
875
+ function (fDone)
876
+ {
877
+ _SuperTest
878
+ .get(`1.0/Book/${_DeletedRecordID}`)
879
+ .end(
880
+ (pError, pResponse) =>
881
+ {
882
+ let tmpResult = JSON.parse(pResponse.text);
883
+ // Record should not be found (soft-deleted)
884
+ Expect(tmpResult).to.have.property('Error');
885
+ fDone();
886
+ }
887
+ );
888
+ }
889
+ );
890
+ test
891
+ (
892
+ 'undelete: restore a soft-deleted record',
893
+ function (fDone)
894
+ {
895
+ _SuperTest
896
+ .get(`1.0/Book/Undelete/${_DeletedRecordID}`)
897
+ .end(
898
+ (pError, pResponse) =>
899
+ {
900
+ let tmpResult = JSON.parse(pResponse.text);
901
+ Expect(tmpResult).to.have.property('Count');
902
+ Expect(tmpResult.Count).to.equal(1);
903
+ fDone();
904
+ }
905
+ );
906
+ }
907
+ );
908
+ test
909
+ (
910
+ 'undelete: the restored record should be readable again',
911
+ function (fDone)
912
+ {
913
+ _SuperTest
914
+ .get(`1.0/Book/${_DeletedRecordID}`)
915
+ .end(
916
+ (pError, pResponse) =>
917
+ {
918
+ let tmpResult = JSON.parse(pResponse.text);
919
+ Expect(tmpResult.IDBook).to.equal(_DeletedRecordID);
920
+ Expect(tmpResult.Title).to.equal('To Be Deleted');
921
+ fDone();
922
+ }
923
+ );
924
+ }
925
+ );
926
+ test
927
+ (
928
+ 'delete: delete and verify zero count for non-existent ID',
929
+ function (fDone)
930
+ {
931
+ _SuperTest
932
+ .delete('1.0/Book/99999')
933
+ .end(
934
+ (pError, pResponse) =>
935
+ {
936
+ let tmpResult = JSON.parse(pResponse.text);
937
+ // Non-existent record should error
938
+ Expect(tmpResult).to.have.property('Error');
939
+ fDone();
940
+ }
941
+ );
942
+ }
943
+ );
944
+ }
945
+ );
946
+
947
+ // ======================================================================
948
+ // Count Endpoints
949
+ // ======================================================================
950
+ suite
951
+ (
952
+ 'Count',
953
+ () =>
954
+ {
955
+ test
956
+ (
957
+ 'count: get total record count',
958
+ function (fDone)
959
+ {
960
+ _SuperTest
961
+ .get('1.0/Books/Count')
962
+ .end(
963
+ (pError, pResponse) =>
964
+ {
965
+ let tmpResult = JSON.parse(pResponse.text);
966
+ Expect(tmpResult).to.have.property('Count');
967
+ Expect(tmpResult.Count).to.be.at.least(5);
968
+ fDone();
969
+ }
970
+ );
971
+ }
972
+ );
973
+ test
974
+ (
975
+ 'count by: count by a field value',
976
+ function (fDone)
977
+ {
978
+ _SuperTest
979
+ .get('1.0/Books/Count/By/Genre/Thriller')
980
+ .end(
981
+ (pError, pResponse) =>
982
+ {
983
+ let tmpResult = JSON.parse(pResponse.text);
984
+ Expect(tmpResult).to.have.property('Count');
985
+ Expect(tmpResult.Count).to.be.at.least(2);
986
+ fDone();
987
+ }
988
+ );
989
+ }
990
+ );
991
+ test
992
+ (
993
+ 'count: count with filter',
994
+ function (fDone)
995
+ {
996
+ _SuperTest
997
+ .get('1.0/Books/Count/FilteredTo/FBV~Genre~LK~%25Science%25')
998
+ .end(
999
+ (pError, pResponse) =>
1000
+ {
1001
+ let tmpResult = JSON.parse(pResponse.text);
1002
+ Expect(tmpResult).to.have.property('Count');
1003
+ Expect(tmpResult.Count).to.be.at.least(3);
1004
+ fDone();
1005
+ }
1006
+ );
1007
+ }
1008
+ );
1009
+ }
1010
+ );
1011
+
1012
+ // ======================================================================
1013
+ // Schema Endpoints
1014
+ // ======================================================================
1015
+ suite
1016
+ (
1017
+ 'Schema',
1018
+ () =>
1019
+ {
1020
+ test
1021
+ (
1022
+ 'schema: get the JSON schema',
1023
+ function (fDone)
1024
+ {
1025
+ _SuperTest
1026
+ .get('1.0/Book/Schema')
1027
+ .end(
1028
+ (pError, pResponse) =>
1029
+ {
1030
+ let tmpResult = JSON.parse(pResponse.text);
1031
+ Expect(tmpResult).to.have.property('title');
1032
+ Expect(tmpResult.title).to.equal('Book');
1033
+ Expect(tmpResult).to.have.property('properties');
1034
+ Expect(tmpResult.properties).to.have.property('IDBook');
1035
+ Expect(tmpResult.properties).to.have.property('Title');
1036
+ Expect(tmpResult.properties).to.have.property('Genre');
1037
+ fDone();
1038
+ }
1039
+ );
1040
+ }
1041
+ );
1042
+ test
1043
+ (
1044
+ 'new: get a default empty record',
1045
+ function (fDone)
1046
+ {
1047
+ _SuperTest
1048
+ .get('1.0/Book/Schema/New')
1049
+ .end(
1050
+ (pError, pResponse) =>
1051
+ {
1052
+ let tmpResult = JSON.parse(pResponse.text);
1053
+ Expect(tmpResult).to.have.property('IDBook');
1054
+ Expect(tmpResult.IDBook).to.equal(0);
1055
+ Expect(tmpResult).to.have.property('Title');
1056
+ Expect(tmpResult.Title).to.equal('');
1057
+ Expect(tmpResult).to.have.property('PublicationYear');
1058
+ Expect(tmpResult.PublicationYear).to.equal(0);
1059
+ fDone();
1060
+ }
1061
+ );
1062
+ }
1063
+ );
1064
+ test
1065
+ (
1066
+ 'validate: validate a valid record',
1067
+ function (fDone)
1068
+ {
1069
+ _SuperTest
1070
+ .post('1.0/Book/Schema/Validate')
1071
+ .send({
1072
+ IDBook: 1,
1073
+ Title: 'Test Book',
1074
+ Genre: 'Test'
1075
+ })
1076
+ .end(
1077
+ (pError, pResponse) =>
1078
+ {
1079
+ // Validation should succeed
1080
+ Expect(pResponse.status).to.equal(200);
1081
+ fDone();
1082
+ }
1083
+ );
1084
+ }
1085
+ );
1086
+ }
1087
+ );
1088
+
1089
+ // ======================================================================
1090
+ // Behavior Injection
1091
+ // ======================================================================
1092
+ suite
1093
+ (
1094
+ 'Behavior Injection',
1095
+ () =>
1096
+ {
1097
+ test
1098
+ (
1099
+ 'setBehavior: register and trigger a Read-PostOperation hook',
1100
+ function (fDone)
1101
+ {
1102
+ // Register a post-read hook that adds a custom field
1103
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Read-PostOperation',
1104
+ (pRequest, pRequestState, fCallback) =>
1105
+ {
1106
+ pRequestState.Record.CustomField = 'injected';
1107
+ return fCallback();
1108
+ });
1109
+
1110
+ _SuperTest
1111
+ .get('1.0/Book/1')
1112
+ .end(
1113
+ (pError, pResponse) =>
1114
+ {
1115
+ let tmpResult = JSON.parse(pResponse.text);
1116
+ Expect(tmpResult.CustomField).to.equal('injected');
1117
+
1118
+ // Clean up the behavior
1119
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Read-PostOperation'];
1120
+ fDone();
1121
+ }
1122
+ );
1123
+ }
1124
+ );
1125
+ test
1126
+ (
1127
+ 'setBehavior: register and trigger a Reads-QueryConfiguration hook',
1128
+ function (fDone)
1129
+ {
1130
+ // Register a query config hook that limits to Genre=Thriller
1131
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Reads-QueryConfiguration',
1132
+ (pRequest, pRequestState, fCallback) =>
1133
+ {
1134
+ pRequestState.Query.addFilter('Genre', 'Thriller');
1135
+ return fCallback();
1136
+ });
1137
+
1138
+ _SuperTest
1139
+ .get('1.0/Books')
1140
+ .end(
1141
+ (pError, pResponse) =>
1142
+ {
1143
+ let tmpResult = JSON.parse(pResponse.text);
1144
+ Expect(tmpResult).to.be.an('array');
1145
+ tmpResult.forEach((pRecord) =>
1146
+ {
1147
+ Expect(pRecord.Genre).to.equal('Thriller');
1148
+ });
1149
+
1150
+ // Clean up the behavior
1151
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Reads-QueryConfiguration'];
1152
+ fDone();
1153
+ }
1154
+ );
1155
+ }
1156
+ );
1157
+ test
1158
+ (
1159
+ 'setBehavior: Create-PreOperation hook can modify the record',
1160
+ function (fDone)
1161
+ {
1162
+ // Register a pre-create hook that sets a default genre
1163
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Create-PreOperation',
1164
+ (pRequest, pRequestState, fCallback) =>
1165
+ {
1166
+ if (!pRequestState.RecordToCreate.Genre || pRequestState.RecordToCreate.Genre === '')
1167
+ {
1168
+ pRequestState.RecordToCreate.Genre = 'DefaultGenre';
1169
+ }
1170
+ return fCallback();
1171
+ });
1172
+
1173
+ _SuperTest
1174
+ .post('1.0/Book')
1175
+ .send({ Title: 'Hook Default Genre' })
1176
+ .end(
1177
+ (pError, pResponse) =>
1178
+ {
1179
+ let tmpResult = JSON.parse(pResponse.text);
1180
+ Expect(tmpResult.Genre).to.equal('DefaultGenre');
1181
+
1182
+ // Clean up
1183
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Create-PreOperation'];
1184
+ fDone();
1185
+ }
1186
+ );
1187
+ }
1188
+ );
1189
+ test
1190
+ (
1191
+ 'setBehavior: Create-PreOperation hook can reject a request',
1192
+ function (fDone)
1193
+ {
1194
+ // Register a pre-create hook that rejects records without a title
1195
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Create-PreOperation',
1196
+ (pRequest, pRequestState, fCallback) =>
1197
+ {
1198
+ if (!pRequestState.RecordToCreate.Title || pRequestState.RecordToCreate.Title === '')
1199
+ {
1200
+ let tmpError = new Error('Title is required');
1201
+ tmpError.StatusCode = 400;
1202
+ return fCallback(tmpError);
1203
+ }
1204
+ return fCallback();
1205
+ });
1206
+
1207
+ _SuperTest
1208
+ .post('1.0/Book')
1209
+ .send({ Genre: 'No Title Provided' })
1210
+ .end(
1211
+ (pError, pResponse) =>
1212
+ {
1213
+ let tmpResult = JSON.parse(pResponse.text);
1214
+ Expect(tmpResult).to.have.property('Error');
1215
+
1216
+ // Clean up
1217
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Create-PreOperation'];
1218
+ fDone();
1219
+ }
1220
+ );
1221
+ }
1222
+ );
1223
+ test
1224
+ (
1225
+ 'setBehavior: Delete-PreOperation hook can prevent deletion',
1226
+ function (fDone)
1227
+ {
1228
+ // Register a pre-delete hook that blocks deletion
1229
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Delete-PreOperation',
1230
+ (pRequest, pRequestState, fCallback) =>
1231
+ {
1232
+ let tmpError = new Error('Deletion is blocked');
1233
+ tmpError.StatusCode = 403;
1234
+ return fCallback(tmpError);
1235
+ });
1236
+
1237
+ _SuperTest
1238
+ .delete('1.0/Book/1')
1239
+ .end(
1240
+ (pError, pResponse) =>
1241
+ {
1242
+ let tmpResult = JSON.parse(pResponse.text);
1243
+ Expect(tmpResult).to.have.property('Error');
1244
+
1245
+ // Clean up
1246
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Delete-PreOperation'];
1247
+ fDone();
1248
+ }
1249
+ );
1250
+ }
1251
+ );
1252
+ test
1253
+ (
1254
+ 'setBehavior: Count-QueryConfiguration hook modifies count query',
1255
+ function (fDone)
1256
+ {
1257
+ // Register a hook that adds a filter to the count query
1258
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Count-QueryConfiguration',
1259
+ (pRequest, pRequestState, fCallback) =>
1260
+ {
1261
+ pRequestState.Query.addFilter('Genre', 'Thriller');
1262
+ return fCallback();
1263
+ });
1264
+
1265
+ _SuperTest
1266
+ .get('1.0/Books/Count')
1267
+ .end(
1268
+ (pError, pResponse) =>
1269
+ {
1270
+ let tmpResult = JSON.parse(pResponse.text);
1271
+ Expect(tmpResult.Count).to.be.at.least(1);
1272
+ // The count should be less than total records
1273
+ Expect(tmpResult.Count).to.be.below(20);
1274
+
1275
+ // Clean up
1276
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Count-QueryConfiguration'];
1277
+ fDone();
1278
+ }
1279
+ );
1280
+ }
1281
+ );
1282
+ test
1283
+ (
1284
+ 'setBehavior: Schema-PostOperation hook can modify schema',
1285
+ function (fDone)
1286
+ {
1287
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Schema-PostOperation',
1288
+ (pRequest, pRequestState, fCallback) =>
1289
+ {
1290
+ if (pRequestState.JSONSchema && pRequestState.JSONSchema.properties)
1291
+ {
1292
+ pRequestState.JSONSchema.customMeta = 'injected-metadata';
1293
+ }
1294
+ return fCallback();
1295
+ });
1296
+
1297
+ _SuperTest
1298
+ .get('1.0/Book/Schema')
1299
+ .end(
1300
+ (pError, pResponse) =>
1301
+ {
1302
+ let tmpResult = JSON.parse(pResponse.text);
1303
+ Expect(tmpResult.customMeta).to.equal('injected-metadata');
1304
+
1305
+ // Clean up
1306
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Schema-PostOperation'];
1307
+ fDone();
1308
+ }
1309
+ );
1310
+ }
1311
+ );
1312
+ test
1313
+ (
1314
+ 'setBehavior: New-PostOperation hook can modify defaults',
1315
+ function (fDone)
1316
+ {
1317
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('New-PostOperation',
1318
+ (pRequest, pRequestState, fCallback) =>
1319
+ {
1320
+ pRequestState.EmptyEntityRecord.Genre = 'DefaultFromHook';
1321
+ pRequestState.EmptyEntityRecord.Language = 'English';
1322
+ return fCallback();
1323
+ });
1324
+
1325
+ _SuperTest
1326
+ .get('1.0/Book/Schema/New')
1327
+ .end(
1328
+ (pError, pResponse) =>
1329
+ {
1330
+ let tmpResult = JSON.parse(pResponse.text);
1331
+ Expect(tmpResult.Genre).to.equal('DefaultFromHook');
1332
+ Expect(tmpResult.Language).to.equal('English');
1333
+
1334
+ // Clean up
1335
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['New-PostOperation'];
1336
+ fDone();
1337
+ }
1338
+ );
1339
+ }
1340
+ );
1341
+ test
1342
+ (
1343
+ 'setBehavior: Update-PostOperation hook runs after update',
1344
+ function (fDone)
1345
+ {
1346
+ let _HookCalled = false;
1347
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Update-PostOperation',
1348
+ (pRequest, pRequestState, fCallback) =>
1349
+ {
1350
+ _HookCalled = true;
1351
+ pRequestState.Record.HookWasHere = true;
1352
+ return fCallback();
1353
+ });
1354
+
1355
+ _SuperTest
1356
+ .put('1.0/Book')
1357
+ .send({ IDBook: 5, Title: 'The Da Vinci Code (Hooked)' })
1358
+ .end(
1359
+ (pError, pResponse) =>
1360
+ {
1361
+ let tmpResult = JSON.parse(pResponse.text);
1362
+ Expect(_HookCalled).to.equal(true);
1363
+ Expect(tmpResult.Title).to.equal('The Da Vinci Code (Hooked)');
1364
+
1365
+ // Clean up
1366
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Update-PostOperation'];
1367
+ fDone();
1368
+ }
1369
+ );
1370
+ }
1371
+ );
1372
+ }
1373
+ );
1374
+
1375
+ // ======================================================================
1376
+ // Session Management
1377
+ // ======================================================================
1378
+ suite
1379
+ (
1380
+ 'Session Management',
1381
+ () =>
1382
+ {
1383
+ test
1384
+ (
1385
+ 'header session: use x-trusted-session header',
1386
+ function (fDone)
1387
+ {
1388
+ // Temporarily switch session source
1389
+ let tmpOriginalSource = _MeadowEndpoints.controller.settings.MeadowEndpointsSessionDataSource;
1390
+ _MeadowEndpoints.controller.settings.MeadowEndpointsSessionDataSource = 'Header';
1391
+
1392
+ let tmpHookTriggered = false;
1393
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Read-PreOperation',
1394
+ (pRequest, pRequestState, fCallback) =>
1395
+ {
1396
+ tmpHookTriggered = true;
1397
+ Expect(pRequestState.SessionData.UserID).to.equal(42);
1398
+ Expect(pRequestState.SessionData.CustomerID).to.equal(100);
1399
+ return fCallback();
1400
+ });
1401
+
1402
+ _SuperTest
1403
+ .get('1.0/Book/1')
1404
+ .set('x-trusted-session', JSON.stringify({ UserID: 42, CustomerID: 100 }))
1405
+ .end(
1406
+ (pError, pResponse) =>
1407
+ {
1408
+ Expect(tmpHookTriggered).to.equal(true);
1409
+
1410
+ // Clean up
1411
+ _MeadowEndpoints.controller.settings.MeadowEndpointsSessionDataSource = tmpOriginalSource;
1412
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Read-PreOperation'];
1413
+ fDone();
1414
+ }
1415
+ );
1416
+ }
1417
+ );
1418
+ }
1419
+ );
1420
+
1421
+ // ======================================================================
1422
+ // Endpoint Configuration
1423
+ // ======================================================================
1424
+ suite
1425
+ (
1426
+ 'Endpoint Configuration',
1427
+ () =>
1428
+ {
1429
+ test
1430
+ (
1431
+ 'setBehaviorEndpoint: should accept custom endpoint functions',
1432
+ function (fDone)
1433
+ {
1434
+ // setBehaviorEndpoint should be a function and accept a custom function
1435
+ Expect(_MeadowEndpoints.setBehaviorEndpoint).to.be.a('function');
1436
+ let tmpResult = _MeadowEndpoints.setBehaviorEndpoint('CustomEndpoint', function () {});
1437
+ Expect(tmpResult).to.equal(_MeadowEndpoints); // Should return this for chaining
1438
+ fDone();
1439
+ }
1440
+ );
1441
+ test
1442
+ (
1443
+ 'setBehaviorEndpoint: should ignore non-function endpoints',
1444
+ function (fDone)
1445
+ {
1446
+ let tmpResult = _MeadowEndpoints.setBehaviorEndpoint('BadEndpoint', 'not a function');
1447
+ Expect(tmpResult).to.equal(_MeadowEndpoints);
1448
+ fDone();
1449
+ }
1450
+ );
1451
+ }
1452
+ );
1453
+
1454
+ // ======================================================================
1455
+ // Full CRUD Lifecycle
1456
+ // ======================================================================
1457
+ suite
1458
+ (
1459
+ 'Full CRUD Lifecycle',
1460
+ () =>
1461
+ {
1462
+ let _LifecycleRecordID = 0;
1463
+
1464
+ test
1465
+ (
1466
+ 'lifecycle: create a record',
1467
+ function (fDone)
1468
+ {
1469
+ _SuperTest
1470
+ .post('1.0/Book')
1471
+ .send({ Title: 'Lifecycle Test', Genre: 'Test', ISBN: '000-0000000000', PublicationYear: 2024 })
1472
+ .end(
1473
+ (pError, pResponse) =>
1474
+ {
1475
+ let tmpResult = JSON.parse(pResponse.text);
1476
+ _LifecycleRecordID = tmpResult.IDBook;
1477
+ Expect(tmpResult.Title).to.equal('Lifecycle Test');
1478
+ Expect(_LifecycleRecordID).to.be.above(0);
1479
+ fDone();
1480
+ }
1481
+ );
1482
+ }
1483
+ );
1484
+ test
1485
+ (
1486
+ 'lifecycle: read the created record',
1487
+ function (fDone)
1488
+ {
1489
+ _SuperTest
1490
+ .get(`1.0/Book/${_LifecycleRecordID}`)
1491
+ .end(
1492
+ (pError, pResponse) =>
1493
+ {
1494
+ let tmpResult = JSON.parse(pResponse.text);
1495
+ Expect(tmpResult.IDBook).to.equal(_LifecycleRecordID);
1496
+ Expect(tmpResult.Title).to.equal('Lifecycle Test');
1497
+ Expect(tmpResult.Genre).to.equal('Test');
1498
+ Expect(tmpResult.ISBN).to.equal('000-0000000000');
1499
+ Expect(tmpResult.PublicationYear).to.equal(2024);
1500
+ fDone();
1501
+ }
1502
+ );
1503
+ }
1504
+ );
1505
+ test
1506
+ (
1507
+ 'lifecycle: update the record',
1508
+ function (fDone)
1509
+ {
1510
+ _SuperTest
1511
+ .put('1.0/Book')
1512
+ .send({ IDBook: _LifecycleRecordID, Title: 'Lifecycle Test (Updated)', PublicationYear: 2025 })
1513
+ .end(
1514
+ (pError, pResponse) =>
1515
+ {
1516
+ let tmpResult = JSON.parse(pResponse.text);
1517
+ Expect(tmpResult.IDBook).to.equal(_LifecycleRecordID);
1518
+ Expect(tmpResult.Title).to.equal('Lifecycle Test (Updated)');
1519
+ Expect(tmpResult.PublicationYear).to.equal(2025);
1520
+ fDone();
1521
+ }
1522
+ );
1523
+ }
1524
+ );
1525
+ test
1526
+ (
1527
+ 'lifecycle: verify update via read',
1528
+ function (fDone)
1529
+ {
1530
+ _SuperTest
1531
+ .get(`1.0/Book/${_LifecycleRecordID}`)
1532
+ .end(
1533
+ (pError, pResponse) =>
1534
+ {
1535
+ let tmpResult = JSON.parse(pResponse.text);
1536
+ Expect(tmpResult.Title).to.equal('Lifecycle Test (Updated)');
1537
+ Expect(tmpResult.PublicationYear).to.equal(2025);
1538
+ fDone();
1539
+ }
1540
+ );
1541
+ }
1542
+ );
1543
+ test
1544
+ (
1545
+ 'lifecycle: count should include the record',
1546
+ function (fDone)
1547
+ {
1548
+ _SuperTest
1549
+ .get('1.0/Books/Count/By/Genre/Test')
1550
+ .end(
1551
+ (pError, pResponse) =>
1552
+ {
1553
+ let tmpResult = JSON.parse(pResponse.text);
1554
+ Expect(tmpResult.Count).to.be.at.least(1);
1555
+ fDone();
1556
+ }
1557
+ );
1558
+ }
1559
+ );
1560
+ test
1561
+ (
1562
+ 'lifecycle: delete the record',
1563
+ function (fDone)
1564
+ {
1565
+ _SuperTest
1566
+ .delete(`1.0/Book/${_LifecycleRecordID}`)
1567
+ .end(
1568
+ (pError, pResponse) =>
1569
+ {
1570
+ let tmpResult = JSON.parse(pResponse.text);
1571
+ Expect(tmpResult.Count).to.equal(1);
1572
+ fDone();
1573
+ }
1574
+ );
1575
+ }
1576
+ );
1577
+ test
1578
+ (
1579
+ 'lifecycle: verify record is gone',
1580
+ function (fDone)
1581
+ {
1582
+ _SuperTest
1583
+ .get(`1.0/Book/${_LifecycleRecordID}`)
1584
+ .end(
1585
+ (pError, pResponse) =>
1586
+ {
1587
+ let tmpResult = JSON.parse(pResponse.text);
1588
+ Expect(tmpResult).to.have.property('Error');
1589
+ fDone();
1590
+ }
1591
+ );
1592
+ }
1593
+ );
1594
+ test
1595
+ (
1596
+ 'lifecycle: undelete the record',
1597
+ function (fDone)
1598
+ {
1599
+ _SuperTest
1600
+ .get(`1.0/Book/Undelete/${_LifecycleRecordID}`)
1601
+ .end(
1602
+ (pError, pResponse) =>
1603
+ {
1604
+ let tmpResult = JSON.parse(pResponse.text);
1605
+ Expect(tmpResult.Count).to.equal(1);
1606
+ fDone();
1607
+ }
1608
+ );
1609
+ }
1610
+ );
1611
+ test
1612
+ (
1613
+ 'lifecycle: verify record is back',
1614
+ function (fDone)
1615
+ {
1616
+ _SuperTest
1617
+ .get(`1.0/Book/${_LifecycleRecordID}`)
1618
+ .end(
1619
+ (pError, pResponse) =>
1620
+ {
1621
+ let tmpResult = JSON.parse(pResponse.text);
1622
+ Expect(tmpResult.IDBook).to.equal(_LifecycleRecordID);
1623
+ Expect(tmpResult.Title).to.equal('Lifecycle Test (Updated)');
1624
+ fDone();
1625
+ }
1626
+ );
78
1627
  }
79
1628
  );
80
1629
  }
81
1630
  );
82
1631
  }
83
- );
1632
+ );