@vida-global/core 2.0.10 → 2.1.0

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.
Files changed (49) hide show
  1. package/index.js +2 -0
  2. package/lib/activeRecord/README.md +1 -1
  3. package/lib/activeRecord/baseRecord.js +70 -4
  4. package/lib/cache/index.js +4 -0
  5. package/lib/cache/memoryCache.js +155 -0
  6. package/lib/server/README.md +8 -219
  7. package/lib/server/controllerImporter.js +0 -1
  8. package/lib/server/controllerMixins/callbacks.js +16 -61
  9. package/lib/server/controllerMixins/classScopedRegistry.js +48 -0
  10. package/lib/server/controllerMixins/documentation.js +41 -9
  11. package/lib/server/controllerMixins/renderer.js +140 -99
  12. package/lib/server/controllerMixins/validations.js +57 -0
  13. package/lib/server/doc/callbacks.md +23 -0
  14. package/lib/server/doc/documentation.md +135 -0
  15. package/lib/server/doc/renderer.md +115 -0
  16. package/lib/server/doc/requestDetails.md +52 -0
  17. package/lib/server/doc/validations.md +40 -0
  18. package/lib/server/index.js +1 -1
  19. package/lib/server/openApi/apiDocGenerator.js +426 -0
  20. package/lib/server/openApi/apiDocsGenerator.js +147 -0
  21. package/lib/server/openApi/schemaImporter.js +41 -0
  22. package/lib/server/openApi/schemaRegistry.js +43 -0
  23. package/lib/server/openApi/schemas.js +97 -0
  24. package/lib/server/openApi/tagRegistry.js +27 -0
  25. package/lib/server/server.js +14 -0
  26. package/lib/server/serverController.js +4 -9
  27. package/lib/server/statusTexts.js +38 -0
  28. package/lib/utils/yamlLoader.js +17 -0
  29. package/package.json +4 -2
  30. package/test/activeRecord/baseRecord.test.js +101 -0
  31. package/test/cache/memoryCache.test.js +233 -0
  32. package/test/server/apiDocGenerator.test.js +743 -0
  33. package/test/server/controllerMixins/callbacks.test.js +326 -0
  34. package/test/server/controllerMixins/classScopedRegistry.test.js +95 -0
  35. package/test/server/controllerMixins/documentation.test.js +168 -0
  36. package/test/server/controllerMixins/renderer.test.js +734 -0
  37. package/test/server/controllerMixins/requestDetails.test.js +225 -0
  38. package/test/server/controllerMixins/routing.test.js +82 -0
  39. package/test/server/controllerMixins/validations.test.js +509 -0
  40. package/test/server/openApi/apiDocsGenerator.test.js +306 -0
  41. package/test/server/openApi/helpers/apiDocsPackageFixture/package.json +5 -0
  42. package/test/server/openApi/schemaImporter.test.js +54 -0
  43. package/test/server/openApi/schemaRegistry.test.js +93 -0
  44. package/test/server/openApi/schemas.test.js +109 -0
  45. package/test/server/serverController.test.js +8 -867
  46. package/test/server/statusTexts.test.js +30 -0
  47. package/test/utils/yamlLoader.test.js +43 -0
  48. package/lib/server/apiDocsGenerator.js +0 -86
  49. package/test/server/apiDocsGenerator.test.js +0 -38
@@ -1,32 +1,14 @@
1
- const { BarController } = require('./helpers/controllers/barController');
2
1
  const { FooController } = require('./helpers/controllers/fooController');
3
- const { camelize } = require('inflection');
4
2
  const Errors = require('../../lib/server/errors');
5
- const { logger } = require('../../lib/logger');
6
3
  const { randomPayload } = require('./helpers/serverController');
7
4
  const TestHelpers = require('@vida-global/test-helpers');
8
5
  const {
6
+ VidaServerController,
9
7
  NoActiveStreamError,
10
8
  StreamInProgressError,
11
- VidaServerController
12
9
  } = require('../../lib/server/serverController');
13
10
 
14
11
  describe('VidaServerController', () => {
15
- describe('.actionNames', () => {
16
- it ('returns actions based on method names', () => {
17
- const names = FooController.actionNames;
18
- const expected = ['postIndex',
19
- 'getFooBar',
20
- 'getBazBan',
21
- 'postFooBar',
22
- 'putFooBar',
23
- 'deleteFooBar',
24
- 'patchFooBar'];
25
- expect(names).toEqual(expected);
26
- });
27
- });
28
-
29
-
30
12
  describe('.autoLoadMixins', () => {
31
13
  class AutoLoadTestController extends VidaServerController {
32
14
  static get mixinDirectory() {
@@ -78,69 +60,6 @@ describe('VidaServerController', () => {
78
60
  });
79
61
 
80
62
 
81
- describe('._constructAction', () => {
82
- class UsersController extends VidaServerController {
83
- static get routes() {
84
- return {getBazBan: '/baz/:id/ban'};
85
- }
86
- };
87
-
88
- it ('returns an object with path, method, and action', () => {
89
- const expected = {
90
- method: 'GET',
91
- path: '/users/fooBar',
92
- action: 'getFooBar'
93
- }
94
- expect(UsersController._constructAction('getFooBar', '')).toEqual(expected);
95
- });
96
-
97
- it ('uses a blank path for index actions', () => {
98
- const expected = {
99
- method: 'POST',
100
- path: '/users',
101
- action: 'postIndex'
102
- }
103
- expect(UsersController._constructAction('postIndex', '')).toEqual(expected);
104
- });
105
-
106
- it ('creates a single resource route for Record', () => {
107
- const expected = {
108
- method: 'GET',
109
- path: '/user/:id',
110
- action: 'getRecord'
111
- }
112
- expect(UsersController._constructAction('getRecord', '')).toEqual(expected);
113
- });
114
-
115
- it ('only uses Record when checking for single resource routes', () => {
116
- const expected = {
117
- method: 'GET',
118
- path: '/users/recording',
119
- action: 'getRecording'
120
- }
121
- expect(UsersController._constructAction('getRecording', '')).toEqual(expected);
122
- });
123
-
124
- it ('creates a single sub resource route for RecordFoo', () => {
125
- const expected = {
126
- method: 'GET',
127
- path: '/user/:id/foo',
128
- action: 'getRecordFoo'
129
- }
130
- expect(UsersController._constructAction('getRecordFoo', '')).toEqual(expected);
131
- });
132
-
133
- it ('uses a custom path when defined', () => {
134
- const expected = {
135
- method: 'GET',
136
- path: '/baz/:id/ban',
137
- action: 'getBazBan'
138
- }
139
- expect(UsersController._constructAction('getBazBan', '')).toEqual(expected);
140
- });
141
- });
142
-
143
-
144
63
  describe('#hasParam', () => {
145
64
  let controller;
146
65
  beforeEach(() => {
@@ -359,452 +278,6 @@ describe('VidaServerController', () => {
359
278
  });
360
279
 
361
280
 
362
- describe('.validateParameters', () => {
363
- const paramName1 = TestHelpers.Faker.Text.randomString();
364
- const paramName2 = TestHelpers.Faker.Text.randomString();
365
- const value1 = TestHelpers.Faker.Text.randomString();
366
- const value2 = TestHelpers.Faker.Text.randomString();
367
- const validationType1 = TestHelpers.Faker.Text.randomString();
368
- const validationType2 = TestHelpers.Faker.Text.randomString();
369
- const options1 = [TestHelpers.Faker.Text.randomString()];
370
- const options2 = [TestHelpers.Faker.Text.randomString()];
371
- const error1 = TestHelpers.Faker.Text.randomString()
372
- const error2 = TestHelpers.Faker.Text.randomString()
373
-
374
- let params;
375
- let controller;
376
- let response;
377
- let validator1;
378
- let validator2;
379
- beforeEach(() => {
380
- params = {[paramName1]: value1, [paramName2]: value2};
381
- response = {json: jest.fn(), statusCode: 200};
382
- controller = new FooController({params}, response);
383
- controller.render = jest.fn();
384
- validator1 = jest.fn();
385
- validator2 = jest.fn();
386
- controller[`validate${camelize(validationType1)}`] = validator1;
387
- controller[`validate${camelize(validationType2)}`] = validator2;
388
- });
389
-
390
- const validations = {
391
- [paramName1]: {[validationType1]: options1, [validationType2]: options2},
392
- [paramName2]: {[validationType1]: options2}
393
- };
394
-
395
- it ('runs validation checkers based on inputs', async () => {
396
- await controller.validateParameters(validations);
397
- expect(validator1).toHaveBeenCalledTimes(2);
398
- expect(validator1).toHaveBeenCalledWith(value1, options1);
399
- expect(validator1).toHaveBeenCalledWith(value2, options2);
400
-
401
- expect(validator2).toHaveBeenCalledTimes(1);
402
- expect(validator2).toHaveBeenCalledWith(value1, options2);
403
- });
404
-
405
- it ('throws a `ValidationError` when there\'s an error', async () => {
406
- validator1.mockImplementation(() => error1);
407
- await expect(controller.validateParameters(validations)).rejects.toThrow(Errors.ValidationError);
408
- });
409
-
410
- it ('attaches the failing parameter and error message to the ValidationError fields', async () => {
411
- validator1.mockImplementation(() => error1);
412
- await expect(controller.validateParameters(validations)).rejects.toMatchObject({
413
- fields: {[paramName1]: [error1], [paramName2]: [error1]}
414
- });
415
- });
416
-
417
- it ('throws a `ValidationError` when there are multiple errors', async () => {
418
- validator1.mockImplementation(() => error1);
419
- validator2.mockImplementation(() => error2);
420
-
421
- await expect(controller.validateParameters(validations)).rejects.toThrow(Errors.ValidationError);
422
- });
423
-
424
- it ('sets nothing when there is no error', async () => {
425
- await controller.validateParameters(validations);
426
- expect(controller.render).toHaveBeenCalledTimes(0);
427
- expect(response.statusCode).toEqual(200);
428
- });
429
-
430
- it ('ignores details where there is no validator', async () => {
431
- const validations = {
432
- [paramName2]: {[validationType1]: options2, 'foo': true}
433
- };
434
- await controller.validateParameters(validations);
435
-
436
- expect(validator1).toHaveBeenCalledTimes(1);
437
- expect(validator1).toHaveBeenCalledWith(value2, options2);
438
- });
439
-
440
- it ('does not error when optional parameters are not included', async () => {
441
- const validations = {foo: {isString: true, optional: true}};
442
- await expect(controller.validateParameters(validations)).resolves.not.toThrow();
443
- });
444
-
445
- it ('does error when optional parameters are invalid', async () => {
446
- const validations = {foo: {isString: {length: {gte: 2}}, optional: true}};
447
- params.foo = '';
448
-
449
- await expect(controller.validateParameters(validations)).rejects.toThrow(Errors.ValidationError);
450
- });
451
- });
452
-
453
-
454
- describe('.validateAction', () => {
455
- it ('is a no-op when the controller has no parametersFor<Action> method', async () => {
456
- class C extends VidaServerController {}
457
- const controller = new C({}, {});
458
- controller.validateParameters = jest.fn();
459
-
460
- await controller.validateAction('getThing');
461
- expect(controller.validateParameters).not.toHaveBeenCalled();
462
- });
463
-
464
- it ('calls validateParameters with the value returned by parametersFor<Action>', async () => {
465
- const params = {foo: {isString: true}};
466
- class C extends VidaServerController {
467
- static parametersForGetThing() { return params; }
468
- }
469
- const controller = new C({}, {});
470
- controller.validateParameters = jest.fn();
471
-
472
- await controller.validateAction('getThing');
473
- expect(controller.validateParameters).toHaveBeenCalledTimes(1);
474
- expect(controller.validateParameters).toHaveBeenCalledWith(params);
475
- });
476
-
477
- it ('camelizes the action name when looking up the parametersFor method', async () => {
478
- const params = {foo: {isString: true}};
479
- class C extends VidaServerController {
480
- static parametersForGetFooBar() { return params; }
481
- }
482
- const controller = new C({}, {});
483
- controller.validateParameters = jest.fn();
484
-
485
- await controller.validateAction('getFooBar');
486
- expect(controller.validateParameters).toHaveBeenCalledWith(params);
487
- });
488
-
489
- it ('propagates ValidationError thrown by validateParameters', async () => {
490
- class C extends VidaServerController {
491
- static parametersForGetThing() { return {foo: {isString: true}}; }
492
- }
493
- const request = { params: {}, query: {}, body: {foo: 42} };
494
- const controller = new C(request, {});
495
-
496
- await expect(controller.validateAction('getThing')).rejects.toThrow(Errors.ValidationError);
497
- });
498
- });
499
-
500
-
501
- describe('.parametersForAction', () => {
502
- it ('returns null when no parametersFor method is defined', () => {
503
- class C extends VidaServerController {}
504
- expect(C.parametersForAction('getThing')).toBe(null);
505
- });
506
-
507
- it ('returns the value of the matching parametersFor<Action> method', () => {
508
- const params = {foo: {isString: true}};
509
- class C extends VidaServerController {
510
- static parametersForGetThing() { return params; }
511
- }
512
- expect(C.parametersForAction('getThing')).toBe(params);
513
- });
514
- });
515
-
516
-
517
- describe('.parametersMethodForAction', () => {
518
- it ('returns the camelized parametersFor<Action> method name', () => {
519
- class C extends VidaServerController {}
520
- expect(C.parametersMethodForAction('getFooBar')).toBe('parametersForGetFooBar');
521
- expect(C.parametersMethodForAction('postIndex')).toBe('parametersForPostIndex');
522
- });
523
- });
524
-
525
-
526
- describe('.validatePresence', () => {
527
- const controller = new FooController({}, {});
528
- it ('returns undefined when there is a value', () => {
529
- const result = controller.validatePresence(TestHelpers.Faker.Text.randomString());
530
- expect(result).toBe(undefined);
531
- });
532
-
533
- it ('returns an error when there is no value', () => {
534
- const result = controller.validatePresence();
535
- expect(result).toEqual('required');
536
- });
537
- });
538
-
539
-
540
- describe('.validateIsInteger', () => {
541
- const controller = new FooController({}, {});
542
- it ('returns undefined when there is an integer', () => {
543
- const val = Math.ceil(Math.random() * 100);
544
- const result = controller.validateIsInteger(val)
545
- expect(result).toEqual(undefined);
546
- });
547
-
548
- it ('returns undefined when there is an integer string', () => {
549
- const val = Math.ceil(Math.random() * 100).toString();
550
- const result = controller.validateIsInteger(val)
551
- expect(result).toEqual(undefined);
552
- });
553
-
554
- it ('returns an error when there is a non integer number', () => {
555
- const val = Math.random() * 100;
556
- const result = controller.validateIsInteger(val)
557
- expect(result).toEqual('must be an integer');
558
- });
559
-
560
- it ('returns an error when there is a non integer string', () => {
561
- // starts with a number because parseFloat converts a string to a float if it starts with a number
562
- const val = '1' + TestHelpers.Faker.Text.randomString();
563
- const result = controller.validateIsInteger(val)
564
- expect(result).toEqual('must be an integer');
565
- });
566
-
567
- it ('returns an error when the value is null', () => {
568
- const result = controller.validateIsInteger(null)
569
- expect(result).toEqual('must be an integer');
570
- });
571
-
572
- it ('returns an error when the value is undefined', () => {
573
- const result = controller.validateIsInteger(undefined)
574
- expect(result).toEqual('must be an integer');
575
- });
576
-
577
- it ('returns an error when the value is an object', () => {
578
- const result = controller.validateIsInteger({})
579
- expect(result).toEqual('must be an integer');
580
- });
581
-
582
- it ('returns an error when the number is less than the gte option', () => {
583
- const result = controller.validateIsInteger(1, {gte: 2});
584
- expect(result).toEqual('must be greater than or equal to 2');
585
- });
586
-
587
- it ('returns undefined when the number is greater than the gte option', () => {
588
- const result = controller.validateIsInteger(3, {gte: 2});
589
- expect(result).toEqual(undefined);
590
- });
591
-
592
- it ('returns undefined when the number is equal to the gte option', () => {
593
- const result = controller.validateIsInteger(2, {gte: 2});
594
- expect(result).toEqual(undefined);
595
- });
596
-
597
- it ('returns an error when the number is greater than the lte option', () => {
598
- const result = controller.validateIsInteger(3, {lte: 2});
599
- expect(result).toEqual('must be less than or equal to 2');
600
- });
601
-
602
- it ('returns undefined when the number is less than the lte option', () => {
603
- const result = controller.validateIsInteger(1, {lte: 2});
604
- expect(result).toEqual(undefined);
605
- });
606
-
607
- it ('returns undefined when the number is equal to the lte option', () => {
608
- const result = controller.validateIsInteger(2, {lte: 2});
609
- expect(result).toEqual(undefined);
610
- });
611
-
612
- it ('supports both the lte and gte options', () => {
613
- let result = controller.validateIsInteger(1, {lte: 2, gte: 0});
614
- expect(result).toEqual(undefined);
615
-
616
- result = controller.validateIsInteger(-1, {lte: 2, gte: 0});
617
- expect(result).toEqual('must be greater than or equal to 0');
618
-
619
- result = controller.validateIsInteger(3, {lte: 2, gte: 0});
620
- expect(result).toEqual('must be less than or equal to 2');
621
- });
622
- });
623
-
624
-
625
- describe('.validateIsString', () => {
626
- const controller = new FooController({}, {});
627
- it ('returns undefined when there is an string', () => {
628
- const result = controller.validateIsString('')
629
- expect(result).toEqual(undefined);
630
- });
631
-
632
- it ('returns an error when there is a number', () => {
633
- const result = controller.validateIsString(Math.random());
634
- expect(result).toEqual('must be a string');
635
- });
636
-
637
- it ('returns an error when the value is null', () => {
638
- const result = controller.validateIsString(null)
639
- expect(result).toEqual('must be a string');
640
- });
641
-
642
- it ('returns an error when the value is undefined', () => {
643
- const result = controller.validateIsString(undefined)
644
- expect(result).toEqual('must be a string');
645
- });
646
-
647
- it ('returns an error when the value is an object', () => {
648
- const result = controller.validateIsString({})
649
- expect(result).toEqual('must be a string');
650
- });
651
-
652
- it ('returns an error when the length is fewer than the gte option', () => {
653
- const result = controller.validateIsString('f', {length: {gte: 2}});
654
- expect(result).toEqual('must be greater than or equal to 2 characters');
655
- });
656
-
657
- it ('returns undefined when the length is greater than the gte option', () => {
658
- const result = controller.validateIsString('foo', {length: {gte: 2}});
659
- expect(result).toEqual(undefined);
660
- });
661
-
662
- it ('returns undefined when the length is equal to the gte option', () => {
663
- const result = controller.validateIsString('fo', {length: {gte: 2}});
664
- expect(result).toEqual(undefined);
665
- });
666
-
667
- it ('returns an error when the length is greater than the lte option', () => {
668
- const result = controller.validateIsString('foo', {length: {lte: 2}});
669
- expect(result).toEqual('must be fewer than or equal to 2 characters');
670
- });
671
-
672
- it ('returns undefined when the length is fewer than the lte option', () => {
673
- const result = controller.validateIsString('f', {length: {lte: 2}});
674
- expect(result).toEqual(undefined);
675
- });
676
-
677
- it ('returns undefined when the length is equal to the lte option', () => {
678
- const result = controller.validateIsString('fo', {length: {lte: 2}});
679
- expect(result).toEqual(undefined);
680
- });
681
-
682
- it ('supports both the lte and gte options', () => {
683
- let result = controller.validateIsString('fo', {length: {lte: 3, gte: 1}});
684
- expect(result).toEqual(undefined);
685
-
686
- result = controller.validateIsString('', {length: {lte: 3, gte: 1}});
687
- expect(result).toEqual('must be greater than or equal to 1 characters');
688
-
689
- result = controller.validateIsString('foobar', {length: {lte: 3, gte: 1}});
690
- expect(result).toEqual('must be fewer than or equal to 3 characters');
691
- });
692
-
693
- it ('returns undefined when the value matches the pattern', () => {
694
- const result = controller.validateIsString('foo', {regex: /oo/})
695
- expect(result).toBe(undefined);
696
- });
697
-
698
- it ('returns an error when the value does not match the pattern', () => {
699
- const result = controller.validateIsString('bar', {regex: /oo/})
700
- expect(result).toBe(`must match /oo/`);
701
- });
702
- });
703
-
704
-
705
- describe('.validateIsDateTime', () => {
706
- const controller = new FooController({}, {});
707
- it ('returns undefined when passed a date string of format YYYY-MM-DD', () => {
708
- const result = controller.validateIsDateTime('2025-12-17');
709
- expect(result).toBe(undefined);
710
- });
711
-
712
- it ('returns undefined when passed a date string of format MM/DD/YY', () => {
713
- const result = controller.validateIsDateTime('12/17/25');
714
- expect(result).toBe(undefined);
715
- });
716
-
717
- it ('returns undefined when passed a date string of format Month name DD, YYYY', () => {
718
- const result = controller.validateIsDateTime('December 17, 2025');
719
- expect(result).toBe(undefined);
720
- });
721
-
722
- it ('returns undefined when passed a datetime string of format YYYY-MM-DD HH:MM:SS', () => {
723
- const result = controller.validateIsDateTime('2025-12-17 01:05:00');
724
- expect(result).toBe(undefined);
725
- });
726
-
727
- it ('returns undefined when passed a datetime string of format MM/DD/YY HH:MM:SS', () => {
728
- const result = controller.validateIsDateTime('12/17/25 01:05:00');
729
- expect(result).toBe(undefined);
730
- });
731
-
732
- it ('returns undefined when passed a datetime string of format Month name DD, YYYY HH:MM:SS', () => {
733
- const result = controller.validateIsDateTime('December 17, 2025 01:05:00');
734
- expect(result).toBe(undefined);
735
- });
736
-
737
- it ('returns an error when passed an invalid date', () => {
738
- const result = controller.validateIsDateTime('foo');
739
- expect(result).toEqual('invalid date');
740
- });
741
- });
742
-
743
-
744
- describe('.validateIsEnum', () => {
745
- const controller = new FooController({}, {});
746
- const enums = [Math.random(), Math.random(), Math.random()];
747
- const error = TestHelpers.Faker.Text.randomString();
748
- const options = { enums, error }
749
- it ('returns undefined when passed a valid value', () => {
750
- const result = controller.validateIsEnum(enums[2], options);
751
- expect(result).toBe(undefined);
752
- });
753
-
754
- it ('returns an error when passed an invalid date', () => {
755
- const result = controller.validateIsEnum('foo', options);
756
- expect(result).toBe(error);
757
- });
758
- });
759
-
760
-
761
- describe('.validateIsBoolean', () => {
762
- const controller = new FooController({}, {});
763
- it ('returns undefined when passed a matching boolean', () => {
764
- const result1 = controller.validateIsBoolean(true, true);
765
- const result2 = controller.validateIsBoolean(false, false);
766
- expect(result1).toBe(undefined);
767
- expect(result2).toBe(undefined);
768
- });
769
-
770
- it ('returns an error when passed an non matching boolean', () => {
771
- const result1 = controller.validateIsBoolean(true, false);
772
- const result2 = controller.validateIsBoolean(false, true);
773
- expect(result1).toBe('must be false');
774
- expect(result2).toBe('must be true');
775
- });
776
-
777
- it ('returns an error when passed an non boolean', () => {
778
- const result1 = controller.validateIsBoolean('', false);
779
- const result2 = controller.validateIsBoolean(0, false);
780
- const result3 = controller.validateIsBoolean('foo', true);
781
- const result4 = controller.validateIsBoolean(1, true);
782
- expect(result1).toBe('must be false');
783
- expect(result2).toBe('must be false');
784
- expect(result3).toBe('must be true');
785
- expect(result4).toBe('must be true');
786
- });
787
- });
788
-
789
-
790
- describe('.validateFunction', () => {
791
- const controller = new FooController({}, {});
792
- const correctValue = Math.random();
793
- const error = TestHelpers.Faker.Text.randomString();
794
- const validator = (value) => value == correctValue ? undefined : error;
795
-
796
- it ('returns undefined when passed a valid value', async () => {
797
- const result = await controller.validateFunction(correctValue, validator);
798
- expect(result).toBe(undefined);
799
- });
800
-
801
- it ('returns an error when the function returns an error', async () => {
802
- const result = await controller.validateFunction(Math.random(), validator);
803
- expect(result).toBe(error);
804
- });
805
- });
806
-
807
-
808
281
  describe('#performRequest', () => {
809
282
  const errorHandlers = [
810
283
  ['renderUnauthorized', 'AuthorizationError'],
@@ -865,7 +338,7 @@ describe('VidaServerController', () => {
865
338
  });
866
339
 
867
340
  it ('awaits render before calling response.end (regression: ERR_HTTP_HEADERS_SENT)', async () => {
868
- const response = { json: jest.fn(), end: jest.fn(), statusCode: 200 };
341
+ const response = { json: jest.fn(), send: jest.fn(), setHeader: jest.fn(), end: jest.fn(), statusCode: 200 };
869
342
  const controller = new FooController({}, response);
870
343
  controller.testAction = jest.fn(async () => ({foo: TestHelpers.Faker.Text.randomString()}));
871
344
 
@@ -880,266 +353,6 @@ describe('VidaServerController', () => {
880
353
  });
881
354
 
882
355
 
883
- describe('Rendering', () => {
884
- let controller;
885
- let response;
886
- beforeEach(() => {
887
- response = {json: jest.fn(), statusCode: 200};
888
- controller = new FooController({}, response);
889
- controller.render = jest.fn();
890
- });
891
-
892
- describe('#renderErrors', () => {
893
- it ('returns a 400 response', async () => {
894
- await controller.renderErrors();
895
- expect(response.statusCode).toBe(400);
896
- })
897
-
898
- it ('includes a blank message and blank fields by default', async () => {
899
- await controller.renderErrors();
900
- expect(controller.render).toHaveBeenCalledTimes(1);
901
- expect(controller.render).toHaveBeenCalledWith({errors: {message: null, fields: {}}});
902
- })
903
-
904
- it ('includes a provided message', async () => {
905
- const msg = TestHelpers.Faker.Text.randomString();
906
- await controller.renderErrors(msg);
907
- expect(controller.render).toHaveBeenCalledTimes(1);
908
- expect(controller.render).toHaveBeenCalledWith({errors: {message: msg, fields: {}}});
909
- });
910
-
911
- it ('includes provided field data', async () => {
912
- const field1 = TestHelpers.Faker.Text.randomString();
913
- const msg1 = TestHelpers.Faker.Text.randomString();
914
- const msg2 = TestHelpers.Faker.Text.randomString();
915
- await controller.renderErrors(null, {[field1]: [msg1, msg2]});
916
- expect(controller.render).toHaveBeenCalledTimes(1);
917
- expect(controller.render).toHaveBeenCalledWith({errors: {message: null, fields: {[field1]: [msg1, msg2]}}});
918
- });
919
-
920
- it ('includes provided message field data', async () => {
921
- const field1 = TestHelpers.Faker.Text.randomString();
922
- const msg1 = TestHelpers.Faker.Text.randomString();
923
- const msg2 = TestHelpers.Faker.Text.randomString();
924
- const msg3 = TestHelpers.Faker.Text.randomString();
925
- await controller.renderErrors(msg1, {[field1]: [msg2, msg3]});
926
- expect(controller.render).toHaveBeenCalledTimes(1);
927
- expect(controller.render).toHaveBeenCalledWith({errors: {message: msg1, fields: {[field1]: [msg2, msg3]}}});
928
- });
929
-
930
- it ('uses the first parameter as fields if it is an object', async () => {
931
- const field1 = TestHelpers.Faker.Text.randomString();
932
- const msg1 = TestHelpers.Faker.Text.randomString();
933
- const msg2 = TestHelpers.Faker.Text.randomString();
934
- await controller.renderErrors({[field1]: [msg1, msg2]});
935
- expect(controller.render).toHaveBeenCalledTimes(1);
936
- expect(controller.render).toHaveBeenCalledWith({errors: {message: null, fields: {[field1]: [msg1, msg2]}}});
937
- });
938
-
939
- it ('converts single field error messages to arrays', async () => {
940
- const field1 = TestHelpers.Faker.Text.randomString();
941
- const msg1 = TestHelpers.Faker.Text.randomString();
942
- await controller.renderErrors({[field1]: msg1});
943
- expect(controller.render).toHaveBeenCalledTimes(1);
944
- expect(controller.render).toHaveBeenCalledWith({errors: {message: null, fields: {[field1]: [msg1]}}});
945
- });
946
- });
947
-
948
-
949
- const successHandlers = [
950
- ['renderCreationSuccessful', 201],
951
- ['renderAccepted', 202],
952
- ['renderNoConent', 204],
953
- ['renderMovedPermanently', 301],
954
- ['renderFound', 302],
955
- ['renderNotModified', 304],
956
- ['renderTemporaryRedirect', 307],
957
- ['renderPermanentRedirect', 308],
958
- ]
959
- describe.each(successHandlers)('#%s', (handlerName, statusCode) => {
960
- it (`returns a ${statusCode} response`, async () => {
961
- await (controller[handlerName]());
962
- expect(response.statusCode).toBe(statusCode);
963
- })
964
-
965
- it ('includes the provided body', async () => {
966
- const key = TestHelpers.Faker.Text.randomString();
967
- const value = TestHelpers.Faker.Text.randomString();
968
- const body = {[key]: value};
969
- await (controller[handlerName](body));
970
-
971
- expect(controller.render).toHaveBeenCalledTimes(1);
972
- expect(controller.render).toHaveBeenCalledWith(body);
973
- });
974
- });
975
-
976
-
977
- const errorHandlers = [
978
- ['renderUnauthorized', 401],
979
- ['renderPaymentRequired', 402],
980
- ['renderForbidden', 403],
981
- ['renderNotFound', 404],
982
- ['renderMethodNotAllowed', 405],
983
- ['renderRequestTimeout', 408],
984
- ['renderConflict', 409],
985
- ['renderGone', 410],
986
- ['renderContentTooLarge', 413],
987
- ['renderUnsupportedMediaType', 415],
988
- ['renderUnprocessableContent', 422],
989
- ['renderLocked', 423],
990
- ['renderTooManyRequests', 429],
991
- ['renderServerError', 500],
992
- ['renderNotImplemented', 501],
993
- ['renderBadGateway', 502],
994
- ['renderServiceUnavailable', 503],
995
- ['renderGatewayTimeout', 504],
996
- ]
997
-
998
- describe.each(errorHandlers)('#%s', (handlerName, statusCode) => {
999
- it (`returns a ${statusCode} response`, async () => {
1000
- await (controller[handlerName]());
1001
- expect(response.statusCode).toBe(statusCode);
1002
- })
1003
-
1004
- it ('includes a blank message by default', async () => {
1005
- await controller[handlerName]();
1006
- expect(controller.render).toHaveBeenCalledTimes(1);
1007
- expect(controller.render).toHaveBeenCalledWith({errors: {message: null}});
1008
- })
1009
-
1010
- it ('includes a provided message', async () => {
1011
- const msg = TestHelpers.Faker.Text.randomString();
1012
- await controller[handlerName](msg);
1013
- expect(controller.render).toHaveBeenCalledTimes(1);
1014
- expect(controller.render).toHaveBeenCalledWith({errors: {message: msg}});
1015
- });
1016
-
1017
- it ('includes a provided body and message', async () => {
1018
- const key = TestHelpers.Faker.Text.randomString();
1019
- const val = TestHelpers.Faker.Text.randomString();
1020
- const body = {[key]: val};
1021
- const msg = TestHelpers.Faker.Text.randomString();
1022
- const expected = {...body, errors: {message: msg}};
1023
- await controller[handlerName](msg, body);
1024
- expect(controller.render).toHaveBeenCalledTimes(1);
1025
- expect(controller.render).toHaveBeenCalledWith(expected);
1026
- });
1027
- });
1028
- });
1029
-
1030
-
1031
- describe('#render', () => {
1032
- let response;
1033
- let controller;
1034
- beforeEach(() => {
1035
- response = {json: jest.fn(), send: jest.fn(), statusCode: 200};
1036
- controller = new FooController({}, response);
1037
- });
1038
-
1039
- it ('calls response.send (not response.json) when the body is a string', async () => {
1040
- const body = TestHelpers.Faker.Text.randomString();
1041
- await controller.render(body);
1042
-
1043
- expect(response.send).toHaveBeenCalledTimes(1);
1044
- expect(response.send).toHaveBeenCalledWith(body);
1045
- expect(response.json).not.toHaveBeenCalled();
1046
- });
1047
-
1048
- it ('marks the controller as rendered after a string render', async () => {
1049
- await controller.render('hello');
1050
- expect(controller.rendered).toBe(true);
1051
- });
1052
-
1053
- it ('is a no-op when the controller has already been rendered', async () => {
1054
- controller.markRendered();
1055
- await controller.render('hello');
1056
- expect(response.send).not.toHaveBeenCalled();
1057
- expect(response.json).not.toHaveBeenCalled();
1058
- });
1059
-
1060
- it ('is a no-op when the controller is streaming', async () => {
1061
- controller._isStreaming = true;
1062
- await controller.render('hello');
1063
- await controller.render({foo: 'bar'});
1064
-
1065
- expect(response.send).not.toHaveBeenCalled();
1066
- expect(response.json).not.toHaveBeenCalled();
1067
- });
1068
-
1069
- it ('does not mark the controller as rendered when streaming', async () => {
1070
- controller._isStreaming = true;
1071
- await controller.render('hello');
1072
- expect(controller.rendered).toBe(false);
1073
- });
1074
- });
1075
-
1076
-
1077
- describe('#processJSONBody', () => {
1078
- let controller;
1079
- beforeEach(() => {
1080
- controller = new FooController({}, {});
1081
- });
1082
-
1083
- it ('returns null when the body is undefined', async () => {
1084
- expect(await controller.processJSONBody(undefined, {})).toBe(null);
1085
- });
1086
-
1087
- it ('returns null when the body is null', async () => {
1088
- expect(await controller.processJSONBody(null, {})).toBe(null);
1089
- });
1090
-
1091
- it ('returns primitives unchanged', async () => {
1092
- expect(await controller.processJSONBody('hello', {})).toBe('hello');
1093
- expect(await controller.processJSONBody(42, {})).toBe(42);
1094
- expect(await controller.processJSONBody(true, {})).toBe(true);
1095
- });
1096
-
1097
- it ('returns Date instances as their epoch ms via getTime', async () => {
1098
- const date = new Date('2020-01-02T03:04:05Z');
1099
- expect(await controller.processJSONBody(date, {})).toBe(date.getTime());
1100
- });
1101
-
1102
- it ('recursively processes Date values inside objects', async () => {
1103
- const date = new Date('2021-02-03T00:00:00Z');
1104
- const result = await controller.processJSONBody({createdAt: date, name: 'foo'}, {});
1105
- expect(result).toEqual({createdAt: date.getTime(), name: 'foo'});
1106
- });
1107
-
1108
- it ('recursively processes arrays', async () => {
1109
- const date = new Date('2022-03-04T00:00:00Z');
1110
- const result = await controller.processJSONBody([1, date, {ts: date}], {});
1111
- expect(result).toEqual([1, date.getTime(), {ts: date.getTime()}]);
1112
- });
1113
-
1114
- it ('invokes toApiResponse on objects that define it and recurses on the result', async () => {
1115
- const date = new Date('2023-04-05T00:00:00Z');
1116
- const apiModel = {
1117
- async toApiResponse() { return {id: 1, createdAt: date}; }
1118
- };
1119
- const result = await controller.processJSONBody(apiModel, {});
1120
- expect(result).toEqual({id: 1, createdAt: date.getTime()});
1121
- });
1122
-
1123
- it ('passes the options argument through to toApiResponse', async () => {
1124
- const options = {standardize: false, extra: 'flag'};
1125
- const toApi = jest.fn().mockResolvedValue({});
1126
- const apiModel = {toApiResponse: toApi};
1127
-
1128
- await controller.processJSONBody(apiModel, options);
1129
- expect(toApi).toHaveBeenCalledWith(options);
1130
- });
1131
-
1132
- it ('recurses through arrays of objects with toApiResponse', async () => {
1133
- const apiModels = [
1134
- {toApiResponse: async () => ({id: 'a'})},
1135
- {toApiResponse: async () => ({id: 'b'})},
1136
- ];
1137
- const result = await controller.processJSONBody(apiModels, {});
1138
- expect(result).toEqual([{id: 'a'}, {id: 'b'}]);
1139
- });
1140
- });
1141
-
1142
-
1143
356
  describe('Request Properties', () => {
1144
357
  let request;
1145
358
  let response;
@@ -1147,83 +360,11 @@ describe('VidaServerController', () => {
1147
360
 
1148
361
  beforeEach(() => {
1149
362
  request = {headers: randomPayload()};
1150
- response = {headers: randomPayload(), statusCode: TestHelpers.Faker.Math.randomNumber(), json: jest.fn()};
363
+ response = {headers: randomPayload(), statusCode: TestHelpers.Faker.Math.randomNumber(), json: jest.fn(), send: jest.fn(), setHeader: jest.fn()};
1151
364
  request.headers['content-type'] = TestHelpers.Faker.Text.randomString();
1152
365
  controller = new FooController(request, response);
1153
366
  });
1154
367
 
1155
- describe('#requestHeaders', () => {
1156
- it ('returns a copy of the request headers', () => {
1157
- expect(controller.requestHeaders).not.toBe(request.headers);
1158
- expect(JSON.stringify(controller.requestHeaders)).toEqual(JSON.stringify(request.headers));
1159
- });
1160
- });
1161
-
1162
-
1163
- describe('#responseHeaders', () => {
1164
- it ('returns the request headers', () => {
1165
- expect(controller.responseHeaders).toBe(response.headers);
1166
- });
1167
- });
1168
-
1169
-
1170
- describe('#contentType', () => {
1171
- it ('returns the content-type request header', () => {
1172
- expect(controller.contentType).toEqual(request.headers['content-type']);
1173
- });
1174
- });
1175
-
1176
-
1177
- describe('#userAgent', () => {
1178
- it ('returns the user-agent request header', () => {
1179
- const ua = TestHelpers.Faker.Text.randomString();
1180
- request.headers['user-agent'] = ua;
1181
- expect(controller.userAgent).toEqual(ua);
1182
- });
1183
- });
1184
-
1185
-
1186
- describe('#requestBody', () => {
1187
- it ('returns the underlying request body', () => {
1188
- const body = {foo: TestHelpers.Faker.Text.randomString()};
1189
- request.body = body;
1190
- expect(controller.requestBody).toBe(body);
1191
- });
1192
- });
1193
-
1194
-
1195
- describe('#requestId', () => {
1196
- it ('returns the request id', () => {
1197
- request.id = TestHelpers.Faker.Text.randomString();
1198
- expect(controller.requestId).toEqual(request.id);
1199
- });
1200
- });
1201
-
1202
-
1203
- describe('#requestIp', () => {
1204
- it ('returns the request ip', () => {
1205
- request.ip = TestHelpers.Faker.Text.randomString();
1206
- expect(controller.requestIp).toEqual(request.ip);
1207
- });
1208
- });
1209
-
1210
-
1211
- describe('#requestMethod', () => {
1212
- it ('returns the request method', () => {
1213
- request.method = 'POST';
1214
- expect(controller.requestMethod).toEqual('POST');
1215
- });
1216
- });
1217
-
1218
-
1219
- describe('#url', () => {
1220
- it ('returns the request originalUrl', () => {
1221
- request.originalUrl = `/${TestHelpers.Faker.Text.randomString()}`;
1222
- expect(controller.url).toEqual(request.originalUrl);
1223
- });
1224
- });
1225
-
1226
-
1227
368
  describe('#logger', () => {
1228
369
  it ('returns a child of the passed logger, scoped to the request id', () => {
1229
370
  const childLogger = {};
@@ -1383,7 +524,7 @@ describe('VidaServerController', () => {
1383
524
  namedBefore() { order.push('before2') }
1384
525
  testAction() { order.push('action'); return {} }
1385
526
  }
1386
- const response = { json: jest.fn(), end: jest.fn(), statusCode: 200 };
527
+ const response = { json: jest.fn(), send: jest.fn(), setHeader: jest.fn(), end: jest.fn(), statusCode: 200 };
1387
528
  const controller = new C({}, response);
1388
529
 
1389
530
  await controller.performRequest('testAction');
@@ -1400,7 +541,7 @@ describe('VidaServerController', () => {
1400
541
  namedAfter() { order.push('after2') }
1401
542
  testAction() { order.push('action'); return {} }
1402
543
  }
1403
- const response = { json: jest.fn(), end: jest.fn(), statusCode: 200 };
544
+ const response = { json: jest.fn(), send: jest.fn(), setHeader: jest.fn(), end: jest.fn(), statusCode: 200 };
1404
545
  const controller = new C({}, response);
1405
546
 
1406
547
  await controller.performRequest('testAction');
@@ -1413,7 +554,7 @@ describe('VidaServerController', () => {
1413
554
  setupCallbacks() { this.beforeCallback(() => false) }
1414
555
  }
1415
556
  C.prototype.testAction = testAction;
1416
- const response = { json: jest.fn(), end: jest.fn(), statusCode: 200 };
557
+ const response = { json: jest.fn(), send: jest.fn(), setHeader: jest.fn(), end: jest.fn(), statusCode: 200 };
1417
558
  const controller = new C({}, response);
1418
559
 
1419
560
  await controller.performRequest('testAction');
@@ -1429,7 +570,7 @@ describe('VidaServerController', () => {
1429
570
  }
1430
571
  testAction() { return {} }
1431
572
  }
1432
- const response = { json: jest.fn(), end: jest.fn(), statusCode: 200 };
573
+ const response = { json: jest.fn(), send: jest.fn(), setHeader: jest.fn(), end: jest.fn(), statusCode: 200 };
1433
574
  const controller = new C({}, response);
1434
575
 
1435
576
  await controller.performRequest('testAction');
@@ -1445,7 +586,7 @@ describe('VidaServerController', () => {
1445
586
  }
1446
587
  testAction() { return {} }
1447
588
  }
1448
- const response = { json: jest.fn(), end: jest.fn(), statusCode: 200 };
589
+ const response = { json: jest.fn(), send: jest.fn(), setHeader: jest.fn(), end: jest.fn(), statusCode: 200 };
1449
590
  const controller = new C({}, response);
1450
591
 
1451
592
  await controller.performRequest('testAction');